From be5f95018b5d918425fe0dc5ccee77203211a1dd Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 10 Jun 2024 22:14:04 +0200 Subject: [PATCH 01/55] chore: update windows code based on latest flutter template with plugin_ffi --- flutter/example/windows/CMakeLists.txt | 31 +++++--- .../example/windows/flutter/CMakeLists.txt | 10 ++- flutter/example/windows/runner/CMakeLists.txt | 26 ++++++- flutter/example/windows/runner/Runner.rc | 14 ++-- .../example/windows/runner/flutter_window.cpp | 19 +++-- .../example/windows/runner/flutter_window.h | 10 +-- flutter/example/windows/runner/main.cpp | 13 ++-- flutter/example/windows/runner/run_loop.cpp | 66 ----------------- flutter/example/windows/runner/run_loop.h | 40 ----------- .../windows/runner/runner.exe.manifest | 2 +- flutter/example/windows/runner/utils.cpp | 13 ++-- .../example/windows/runner/win32_window.cpp | 55 +++++++++++++-- flutter/example/windows/runner/win32_window.h | 20 +++--- flutter/pubspec.yaml | 2 +- flutter/windows/.gitignore | 3 - flutter/windows/CMakeLists.txt | 27 +++---- .../sentry_flutter/sentry_flutter_plugin.h | 23 ------ flutter/windows/sentry_flutter_plugin.cpp | 70 ------------------- 18 files changed, 163 insertions(+), 281 deletions(-) delete mode 100644 flutter/example/windows/runner/run_loop.cpp delete mode 100644 flutter/example/windows/runner/run_loop.h delete mode 100644 flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h delete mode 100644 flutter/windows/sentry_flutter_plugin.cpp diff --git a/flutter/example/windows/CMakeLists.txt b/flutter/example/windows/CMakeLists.txt index 845ddf6fef..5a554e25d0 100644 --- a/flutter/example/windows/CMakeLists.txt +++ b/flutter/example/windows/CMakeLists.txt @@ -1,13 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) project(sentry_flutter_example LANGUAGES CXX) +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. set(BINARY_NAME "sentry_flutter_example") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -30,6 +33,10 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") add_definitions(-DUNICODE -D_UNICODE) # Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/flutter/example/windows/flutter/CMakeLists.txt b/flutter/example/windows/flutter/CMakeLists.txt index c10f4f62cb..efb62ebe7d 100644 --- a/flutter/example/windows/flutter/CMakeLists.txt +++ b/flutter/example/windows/flutter/CMakeLists.txt @@ -1,4 +1,5 @@ -cmake_minimum_required(VERSION 3.15) +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/flutter/example/windows/runner/CMakeLists.txt b/flutter/example/windows/runner/CMakeLists.txt index e993217632..2041a04410 100644 --- a/flutter/example/windows/runner/CMakeLists.txt +++ b/flutter/example/windows/runner/CMakeLists.txt @@ -1,18 +1,40 @@ -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" - "run_loop.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/example/windows/runner/Runner.rc b/flutter/example/windows/runner/Runner.rc index a059a08d0d..90e64b7b0f 100644 --- a/flutter/example/windows/runner/Runner.rc +++ b/flutter/example/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif @@ -90,10 +90,10 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "Demonstrates how to use the sentry_flutter plugin." "\0" + VALUE "FileDescription", "sentry_flutter_example" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "sentry_flutter_example" "\0" - VALUE "LegalCopyright", "Copyright (C) 2021 com.example. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" VALUE "OriginalFilename", "sentry_flutter_example.exe" "\0" VALUE "ProductName", "sentry_flutter_example" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/flutter/example/windows/runner/flutter_window.cpp b/flutter/example/windows/runner/flutter_window.cpp index ac04f7790a..c819cb083f 100644 --- a/flutter/example/windows/runner/flutter_window.cpp +++ b/flutter/example/windows/runner/flutter_window.cpp @@ -4,9 +4,8 @@ #include "flutter/generated_plugin_registrant.h" -FlutterWindow::FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project) - : run_loop_(run_loop), project_(project) {} +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} FlutterWindow::~FlutterWindow() {} @@ -26,14 +25,22 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); - run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } void FlutterWindow::OnDestroy() { if (flutter_controller_) { - run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); flutter_controller_ = nullptr; } @@ -44,7 +51,7 @@ LRESULT FlutterWindow::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opporutunity to handle window messages. + // Give Flutter, including plugins, an opportunity to handle window messages. if (flutter_controller_) { std::optional result = flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, diff --git a/flutter/example/windows/runner/flutter_window.h b/flutter/example/windows/runner/flutter_window.h index ba86031c6c..28c23839b9 100644 --- a/flutter/example/windows/runner/flutter_window.h +++ b/flutter/example/windows/runner/flutter_window.h @@ -6,16 +6,13 @@ #include -#include "run_loop.h" #include "win32_window.h" // A window that does nothing but host a Flutter view. class FlutterWindow : public Win32Window { public: - // Creates a new FlutterWindow driven by the |run_loop|, hosting a - // Flutter view running |project|. - explicit FlutterWindow(RunLoop* run_loop, - const flutter::DartProject& project); + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); virtual ~FlutterWindow(); protected: @@ -26,9 +23,6 @@ class FlutterWindow : public Win32Window { LPARAM const lparam) noexcept override; private: - // The run loop driving events for this window. - RunLoop* run_loop_; - // The project to run. flutter::DartProject project_; diff --git a/flutter/example/windows/runner/main.cpp b/flutter/example/windows/runner/main.cpp index 0685ffa6aa..11ea9c69a7 100644 --- a/flutter/example/windows/runner/main.cpp +++ b/flutter/example/windows/runner/main.cpp @@ -3,7 +3,6 @@ #include #include "flutter_window.h" -#include "run_loop.h" #include "utils.h" int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, @@ -18,8 +17,6 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, // plugins. ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - RunLoop run_loop; - flutter::DartProject project(L"data"); std::vector command_line_arguments = @@ -27,15 +24,19 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - FlutterWindow window(&run_loop, project); + FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"sentry_flutter_example", origin, size)) { + if (!window.Create(L"sentry_flutter_example", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); - run_loop.Run(); + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } ::CoUninitialize(); return EXIT_SUCCESS; diff --git a/flutter/example/windows/runner/run_loop.cpp b/flutter/example/windows/runner/run_loop.cpp deleted file mode 100644 index 0d912118c2..0000000000 --- a/flutter/example/windows/runner/run_loop.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "run_loop.h" - -#include - -#include - -RunLoop::RunLoop() {} - -RunLoop::~RunLoop() {} - -void RunLoop::Run() { - bool keep_running = true; - TimePoint next_flutter_event_time = TimePoint::clock::now(); - while (keep_running) { - std::chrono::nanoseconds wait_duration = - std::max(std::chrono::nanoseconds(0), - next_flutter_event_time - TimePoint::clock::now()); - ::MsgWaitForMultipleObjects( - 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), - QS_ALLINPUT); - bool processed_events = false; - MSG message; - // All pending Windows messages must be processed; MsgWaitForMultipleObjects - // won't return again for items left in the queue after PeekMessage. - while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { - processed_events = true; - if (message.message == WM_QUIT) { - keep_running = false; - break; - } - ::TranslateMessage(&message); - ::DispatchMessage(&message); - // Allow Flutter to process messages each time a Windows message is - // processed, to prevent starvation. - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - // If the PeekMessage loop didn't run, process Flutter messages. - if (!processed_events) { - next_flutter_event_time = - std::min(next_flutter_event_time, ProcessFlutterMessages()); - } - } -} - -void RunLoop::RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.insert(flutter_instance); -} - -void RunLoop::UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance) { - flutter_instances_.erase(flutter_instance); -} - -RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { - TimePoint next_event_time = TimePoint::max(); - for (auto instance : flutter_instances_) { - std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); - if (wait_duration != std::chrono::nanoseconds::max()) { - next_event_time = - std::min(next_event_time, TimePoint::clock::now() + wait_duration); - } - } - return next_event_time; -} diff --git a/flutter/example/windows/runner/run_loop.h b/flutter/example/windows/runner/run_loop.h deleted file mode 100644 index 54927f9773..0000000000 --- a/flutter/example/windows/runner/run_loop.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef RUNNER_RUN_LOOP_H_ -#define RUNNER_RUN_LOOP_H_ - -#include - -#include -#include - -// A runloop that will service events for Flutter instances as well -// as native messages. -class RunLoop { - public: - RunLoop(); - ~RunLoop(); - - // Prevent copying - RunLoop(RunLoop const&) = delete; - RunLoop& operator=(RunLoop const&) = delete; - - // Runs the run loop until the application quits. - void Run(); - - // Registers the given Flutter instance for event servicing. - void RegisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - // Unregisters the given Flutter instance from event servicing. - void UnregisterFlutterInstance( - flutter::FlutterEngine* flutter_instance); - - private: - using TimePoint = std::chrono::steady_clock::time_point; - - // Processes all currently pending messages for registered Flutter instances. - TimePoint ProcessFlutterMessages(); - - std::set flutter_instances_; -}; - -#endif // RUNNER_RUN_LOOP_H_ diff --git a/flutter/example/windows/runner/runner.exe.manifest b/flutter/example/windows/runner/runner.exe.manifest index 2c680b8be2..157e871fe8 100644 --- a/flutter/example/windows/runner/runner.exe.manifest +++ b/flutter/example/windows/runner/runner.exe.manifest @@ -7,7 +7,7 @@ - + diff --git a/flutter/example/windows/runner/utils.cpp b/flutter/example/windows/runner/utils.cpp index 05b53c01b4..fc55c573b5 100644 --- a/flutter/example/windows/runner/utils.cpp +++ b/flutter/example/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/flutter/example/windows/runner/win32_window.cpp b/flutter/example/windows/runner/win32_window.cpp index 97f4439cd1..b5ba2a099f 100644 --- a/flutter/example/windows/runner/win32_window.cpp +++ b/flutter/example/windows/runner/win32_window.cpp @@ -1,13 +1,31 @@ #include "win32_window.h" +#include #include #include "resource.h" namespace { +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; @@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); } + FreeLibrary(user32_module); } } // namespace @@ -42,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -102,9 +120,9 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); const wchar_t* window_class = @@ -117,7 +135,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); @@ -126,9 +144,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title, return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, @@ -188,6 +212,10 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -243,3 +271,18 @@ bool Win32Window::OnCreate() { void Win32Window::OnDestroy() { // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/flutter/example/windows/runner/win32_window.h b/flutter/example/windows/runner/win32_window.h index d9bcac1b60..49b847f075 100644 --- a/flutter/example/windows/runner/win32_window.h +++ b/flutter/example/windows/runner/win32_window.h @@ -28,15 +28,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -76,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -86,6 +87,9 @@ class Win32Window { // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + bool quit_on_close_ = false; // window handle for top level window. diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index d71721230d..ad952542d8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -56,4 +56,4 @@ flutter: linux: pluginClass: SentryFlutterPlugin windows: - pluginClass: SentryFlutterPlugin + ffiPlugin: true diff --git a/flutter/windows/.gitignore b/flutter/windows/.gitignore index 2c36fa939d..808064a0fa 100644 --- a/flutter/windows/.gitignore +++ b/flutter/windows/.gitignore @@ -15,6 +15,3 @@ x86/ *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ - -flutter/example/windows/flutter/generated_plugins.cmake -flutter/example/windows/flutter/generated_plugin_registrant.* \ No newline at end of file diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 1f0a4aff9d..b7d3459e35 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -1,23 +1,16 @@ -cmake_minimum_required(VERSION 3.15) +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.14 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.14) + +# Project-level configuration. set(PROJECT_NAME "sentry_flutter") project(${PROJECT_NAME} LANGUAGES CXX) -# This value is used when generating builds using this plugin, so it must -# not be changed -set(PLUGIN_NAME "sentry_flutter_plugin") - -add_library(${PLUGIN_NAME} SHARED - "sentry_flutter_plugin.cpp" -) -apply_standard_settings(${PLUGIN_NAME}) -set_target_properties(${PLUGIN_NAME} PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -target_include_directories(${PLUGIN_NAME} INTERFACE - "${CMAKE_CURRENT_SOURCE_DIR}/include") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) - -# List of absolute paths to libraries that should be bundled with the plugin +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. set(sentry_flutter_bundled_libraries "" PARENT_SCOPE diff --git a/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h b/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h deleted file mode 100644 index d8482b94cd..0000000000 --- a/flutter/windows/include/sentry_flutter/sentry_flutter_plugin.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ -#define FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ - -#include - -#ifdef FLUTTER_PLUGIN_IMPL -#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) -#else -#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) -#endif - -#if defined(__cplusplus) -extern "C" { -#endif - -FLUTTER_PLUGIN_EXPORT void SentryFlutterPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar); - -#if defined(__cplusplus) -} // extern "C" -#endif - -#endif // FLUTTER_PLUGIN_SENTRY_FLUTTER_PLUGIN_H_ diff --git a/flutter/windows/sentry_flutter_plugin.cpp b/flutter/windows/sentry_flutter_plugin.cpp deleted file mode 100644 index 638f4ff16e..0000000000 --- a/flutter/windows/sentry_flutter_plugin.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "include/sentry_flutter/sentry_flutter_plugin.h" - -// This must be included before many other Windows headers. -#include - -// For getPlatformVersion; remove unless needed for your plugin implementation. -#include - -#include -#include -#include - -#include -#include -#include - -namespace { - -class SentryFlutterPlugin : public flutter::Plugin { - public: - static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar); - - SentryFlutterPlugin(); - - virtual ~SentryFlutterPlugin(); - - private: - // Called when a method is called on this plugin's channel from Dart. - void HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result); -}; - -// static -void SentryFlutterPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarWindows *registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "sentry_flutter", - &flutter::StandardMethodCodec::GetInstance()); - - auto plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto &call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - - registrar->AddPlugin(std::move(plugin)); -} - -SentryFlutterPlugin::SentryFlutterPlugin() {} - -SentryFlutterPlugin::~SentryFlutterPlugin() {} - -void SentryFlutterPlugin::HandleMethodCall( - const flutter::MethodCall &method_call, - std::unique_ptr> result) { - // Native features will be added in a next release - result->NotImplemented(); -} - -} // namespace - -void SentryFlutterPluginRegisterWithRegistrar( - FlutterDesktopPluginRegistrarRef registrar) { - SentryFlutterPlugin::RegisterWithRegistrar( - flutter::PluginRegistrarManager::GetInstance() - ->GetRegistrar(registrar)); -} From f88531c4a9640e3c3332b21b56280e2c0353cee3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 21 Aug 2024 18:58:04 +0200 Subject: [PATCH 02/55] feat: add sentry-native library to the windows build --- .github/workflows/update-deps.yml | 10 +++- flutter/.gitignore | 2 +- flutter/ffi-native.yaml | 18 ++++++++ flutter/lib/src/native/c/binding.dart | 48 ++++++++++++++++++++ flutter/pubspec.yaml | 4 ++ flutter/scripts/generate-cocoa-bindings.sh | 12 +---- flutter/scripts/generate-native-bindings.ps1 | 20 ++++++++ flutter/scripts/update-native.sh | 29 ++++++++++++ flutter/sentry-native/CMakeCache.txt | 5 ++ flutter/sentry-native/sentry-native.cmake | 15 ++++++ flutter/windows/CMakeLists.txt | 6 +-- 11 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 flutter/ffi-native.yaml create mode 100644 flutter/lib/src/native/c/binding.dart create mode 100644 flutter/scripts/generate-native-bindings.ps1 create mode 100644 flutter/scripts/update-native.sh create mode 100644 flutter/sentry-native/CMakeCache.txt create mode 100644 flutter/sentry-native/sentry-native.cmake diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml index 459b4f3687..b33bfa4b7a 100644 --- a/.github/workflows/update-deps.yml +++ b/.github/workflows/update-deps.yml @@ -3,7 +3,7 @@ name: Update Dependencies on: # Run every day. schedule: - - cron: "0 3 * * *" + - cron: '0 3 * * *' # And on on every PR merge so we get the updated dependencies ASAP, and to make sure the changelog doesn't conflict. push: branches: @@ -27,6 +27,14 @@ jobs: secrets: api-token: ${{ secrets.CI_DEPLOY_KEY }} + native: + uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 + with: + path: flutter/scripts/update-native.sh + name: Native SDK + secrets: + api-token: ${{ secrets.CI_DEPLOY_KEY }} + metrics-flutter: uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 with: diff --git a/flutter/.gitignore b/flutter/.gitignore index 068bf84155..30db743ffc 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -10,4 +10,4 @@ build/ .cxx/ .vscode/launch.json -cocoa_bindings_temp +temp diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml new file mode 100644 index 0000000000..b99ffcbc24 --- /dev/null +++ b/flutter/ffi-native.yaml @@ -0,0 +1,18 @@ +# Run with `dart ffigen --config ffi-native.yaml`. +name: SentryNative +description: Sentry Native SDK FFI binding. +output: lib/src/native/c/binding.dart +headers: + entry-points: + - ./temp/sentry-native.h +exclude-all-by-default: true +functions: + include: + - sentry_init +structs: + dependency-only: opaque +unions: + dependency-only: opaque +comments: + style: any + length: full diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart new file mode 100644 index 0000000000..d1e6fdae8d --- /dev/null +++ b/flutter/lib/src/native/c/binding.dart @@ -0,0 +1,48 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +/// Sentry Native SDK FFI binding. +class SentryNative { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + SentryNative(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + SentryNative.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + /// Initializes the Sentry SDK with the specified options. + /// + /// This takes ownership of the options. After the options have been set + /// they cannot be modified any more. + /// Depending on the configured transport and backend, this function might not be + /// fully thread-safe. + /// Returns 0 on success. + int sentry_init( + ffi.Pointer options, + ) { + return _sentry_init( + options, + ); + } + + late final _sentry_initPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_init'); + late final _sentry_init = + _sentry_initPtr.asFunction)>(); +} + +/// The Sentry Client Options. +/// +/// See https://docs.sentry.io/platforms/native/configuration/ +class sentry_options_s extends ffi.Opaque {} diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ad952542d8..7624bd8b9f 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -39,6 +39,10 @@ dev_dependencies: remove_from_coverage: ^2.0.0 flutter_localizations: sdk: flutter + ffigen: + git: + url: https://github.com/getsentry/ffigen + ref: 6aa2c2642f507eab3df83373189170797a9fa5e7 flutter: plugin: diff --git a/flutter/scripts/generate-cocoa-bindings.sh b/flutter/scripts/generate-cocoa-bindings.sh index 9af4cb2084..3415ee3f62 100755 --- a/flutter/scripts/generate-cocoa-bindings.sh +++ b/flutter/scripts/generate-cocoa-bindings.sh @@ -14,18 +14,8 @@ cocoa_version="${1:-$(./scripts/update-cocoa.sh get-version)}" cd "$(dirname "$0")/../" -# Remove dependency on script exit (even in case of an error). -trap "dart pub remove ffigen" EXIT - -# Currently we add the dependency only when the code needs to be generated because it depends -# on Dart SDK 3.2.0 which isn't available on with Flutter stable yet. -# Leaving the dependency in pubspec would block all contributors. -# As for why this is coming from a fork - because we need a specific version of ffigen including PR 607 but not PR 601 -# which starts generating code not compatible with Dart SDK 2.17. The problem is they were merged in the wrong order... -dart pub add 'dev:ffigen:{"git":{"url":"https://github.com/getsentry/ffigen","ref":"6aa2c2642f507eab3df83373189170797a9fa5e7"}}' - # Download Cocoa SDK (we need the headers) -temp="cocoa_bindings_temp" +temp="temp" rm -rf $temp mkdir -p $temp curl -Lv --fail-with-body https://github.com/getsentry/sentry-cocoa/releases/download/$cocoa_version/Sentry.xcframework.zip -o $temp/Sentry.xcframework.zip diff --git a/flutter/scripts/generate-native-bindings.ps1 b/flutter/scripts/generate-native-bindings.ps1 new file mode 100644 index 0000000000..7ebab9b597 --- /dev/null +++ b/flutter/scripts/generate-native-bindings.ps1 @@ -0,0 +1,20 @@ +# This is a PowerShell script instead of a bash script because it needs to run on Windows during local development. +Push-Location "$PSScriptRoot/../" +try +{ + New-Item temp -ItemType Directory -ErrorAction SilentlyContinue | Out-Null + + $props = ConvertFrom-StringData (Get-Content sentry-native/CMakeCache.txt -Raw) + Invoke-WebRequest -Uri "https://raw.githubusercontent.com/getsentry/sentry-native/$($props.version)/include/sentry.h" -OutFile temp/sentry-native.h + + $binding = 'lib/src/native/c/binding.dart' + dart run ffigen --config ffi-native.yaml + $content = Get-Content $binding -Raw + $content = $content -replace 'final class', 'class' + $content | Set-Content -NoNewline -Encoding utf8 $binding + dart format $binding +} +finally +{ + Pop-Location +} diff --git a/flutter/scripts/update-native.sh b/flutter/scripts/update-native.sh new file mode 100644 index 0000000000..abddc3a980 --- /dev/null +++ b/flutter/scripts/update-native.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/../sentry-native" +file='CMakeCache.txt' +content=$(cat $file) +regex="(version)=([0-9\.]+(\-[a-z0-9\.]+)?)" +if ! [[ $content =~ $regex ]]; then + echo "Failed to find the plugin version in $file" + exit 1 +fi + +case $1 in +get-version) + echo "${BASH_REMATCH[2]}" + ;; +get-repo) + echo "$content" | grep -w repo | cut -d '=' -f 2 | tr -d '\n' + ;; +set-version) + newValue="${BASH_REMATCH[1]}=$2" + echo "${content/${BASH_REMATCH[0]}/$newValue}" >$file + pwsh ../scripts/generate-native-bindings.ps1 "$2" + ;; +*) + echo "Unknown argument $1" + exit 1 + ;; +esac diff --git a/flutter/sentry-native/CMakeCache.txt b/flutter/sentry-native/CMakeCache.txt new file mode 100644 index 0000000000..6e747be3f6 --- /dev/null +++ b/flutter/sentry-native/CMakeCache.txt @@ -0,0 +1,5 @@ +# This is not actually a CMakeCache.txt file, but a load_cache() requires the name. +# Basically, this is a properties file we use both in CMake and update-deps.yml to update dependencies. + +repo=https://github.com/getsentry/sentry-native +version=0.7.8 diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake new file mode 100644 index 0000000000..6a8886bdf8 --- /dev/null +++ b/flutter/sentry-native/sentry-native.cmake @@ -0,0 +1,15 @@ +load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo version) +message(STATUS "Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") + +set(SENTRY_SDK_NAME "sentry.native.flutter" CACHE STRING "The SDK name to report when sending events." FORCE) +set(SENTRY_BACKEND "inproc" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) +set(SENTRY_BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" FORCE) + +include(FetchContent) +FetchContent_Declare( + sentry-native + GIT_REPOSITORY ${SENTRY_NATIVE_repo} + GIT_TAG ${SENTRY_NATIVE_version} +) + +FetchContent_MakeAvailable(sentry-native) diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index b7d3459e35..68e4e45e64 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -4,14 +4,12 @@ # customers of the plugin. cmake_minimum_required(VERSION 3.14) -# Project-level configuration. -set(PROJECT_NAME "sentry_flutter") -project(${PROJECT_NAME} LANGUAGES CXX) +include("${CMAKE_CURRENT_SOURCE_DIR}/../sentry-native/sentry-native.cmake") # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an # external build triggered from this build file. set(sentry_flutter_bundled_libraries - "" + $ PARENT_SCOPE ) From 4341827b33c813b890ba7f694b135cb0e9b39f14 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Aug 2024 14:29:11 +0200 Subject: [PATCH 03/55] add sentry-native bindings --- dart/lib/src/platform_checker.dart | 8 +- flutter/ffi-native.yaml | 10 ++ flutter/lib/src/native/c/binding.dart | 157 ++++++++++++++++++- flutter/lib/src/native/c/sentry_native.dart | 141 +++++++++++++++++ flutter/lib/src/native/c/utils.dart | 24 +++ flutter/lib/src/native/factory_real.dart | 3 + flutter/scripts/generate-native-bindings.ps1 | 1 + 7 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 flutter/lib/src/native/c/sentry_native.dart create mode 100644 flutter/lib/src/native/c/utils.dart diff --git a/dart/lib/src/platform_checker.dart b/dart/lib/src/platform_checker.dart index 5169ca25f0..334d73f43f 100644 --- a/dart/lib/src/platform_checker.dart +++ b/dart/lib/src/platform_checker.dart @@ -44,10 +44,10 @@ class PlatformChecker { // the OS checks return true when the browser runs on the checked platform. // Example: platform.isAndroid return true if the browser is used on an // Android device. - if (platform.isAndroid || platform.isIOS || platform.isMacOS) { - return true; - } - return false; + return platform.isAndroid || + platform.isIOS || + platform.isMacOS || + platform.isWindows; } static bool _isWebWithWasmSupport() { diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index b99ffcbc24..f4aa63ef69 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -9,6 +9,16 @@ exclude-all-by-default: true functions: include: - sentry_init + - sentry_options_new + - sentry_options_set_dsn + - sentry_options_set_debug + - sentry_options_set_environment + - sentry_options_set_release + - sentry_options_set_auto_session_tracking + - sentry_options_set_dist + - sentry_options_set_max_breadcrumbs + rename: + 'sentry_(.*)': '$1' structs: dependency-only: opaque unions: diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index d1e6fdae8d..7c3f439670 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -20,6 +20,153 @@ class SentryNative { lookup) : _lookup = lookup; + /// Creates a new options struct. + /// Can be freed with `sentry_options_free`. + ffi.Pointer options_new() { + return _options_new(); + } + + late final _options_newPtr = + _lookup Function()>>( + 'sentry_options_new'); + late final _options_new = + _options_newPtr.asFunction Function()>(); + + /// Sets the DSN. + void options_set_dsn( + ffi.Pointer opts, + ffi.Pointer dsn, + ) { + return _options_set_dsn( + opts, + dsn, + ); + } + + late final _options_set_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dsn'); + late final _options_set_dsn = _options_set_dsnPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Sets the release. + void options_set_release( + ffi.Pointer opts, + ffi.Pointer release, + ) { + return _options_set_release( + opts, + release, + ); + } + + late final _options_set_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_release'); + late final _options_set_release = _options_set_releasePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Sets the environment. + void options_set_environment( + ffi.Pointer opts, + ffi.Pointer environment, + ) { + return _options_set_environment( + opts, + environment, + ); + } + + late final _options_set_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_environment'); + late final _options_set_environment = _options_set_environmentPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Sets the dist. + void options_set_dist( + ffi.Pointer opts, + ffi.Pointer dist, + ) { + return _options_set_dist( + opts, + dist, + ); + } + + late final _options_set_distPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dist'); + late final _options_set_dist = _options_set_distPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Enables or disables debug printing mode. + void options_set_debug( + ffi.Pointer opts, + int debug, + ) { + return _options_set_debug( + opts, + debug, + ); + } + + late final _options_set_debugPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_debug'); + late final _options_set_debug = _options_set_debugPtr + .asFunction, int)>(); + + /// Sets the number of breadcrumbs being tracked and attached to events. + /// + /// Defaults to 100. + void options_set_max_breadcrumbs( + ffi.Pointer opts, + int max_breadcrumbs, + ) { + return _options_set_max_breadcrumbs( + opts, + max_breadcrumbs, + ); + } + + late final _options_set_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Size)>>('sentry_options_set_max_breadcrumbs'); + late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr + .asFunction, int)>(); + + /// Enables or disables automatic session tracking. + /// + /// Automatic session tracking is enabled by default and is equivalent to calling + /// `sentry_start_session` after startup. + /// There can only be one running session, and the current session will always be + /// closed implicitly by `sentry_close`, when starting a new session with + /// `sentry_start_session`, or manually by calling `sentry_end_session`. + void options_set_auto_session_tracking( + ffi.Pointer opts, + int val, + ) { + return _options_set_auto_session_tracking( + opts, + val, + ); + } + + late final _options_set_auto_session_trackingPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_auto_session_tracking'); + late final _options_set_auto_session_tracking = + _options_set_auto_session_trackingPtr + .asFunction, int)>(); + /// Initializes the Sentry SDK with the specified options. /// /// This takes ownership of the options. After the options have been set @@ -27,19 +174,19 @@ class SentryNative { /// Depending on the configured transport and backend, this function might not be /// fully thread-safe. /// Returns 0 on success. - int sentry_init( + int init( ffi.Pointer options, ) { - return _sentry_init( + return _init( options, ); } - late final _sentry_initPtr = _lookup< + late final _initPtr = _lookup< ffi.NativeFunction)>>( 'sentry_init'); - late final _sentry_init = - _sentry_initPtr.asFunction)>(); + late final _init = + _initPtr.asFunction)>(); } /// The Sentry Client Options. diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart new file mode 100644 index 0000000000..354549228d --- /dev/null +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -0,0 +1,141 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import '../../../sentry_flutter.dart'; +import '../native_app_start.dart'; +import '../native_frames.dart'; +import '../sentry_native_binding.dart'; +import '../sentry_native_invoker.dart'; +import 'binding.dart' as binding; +import 'utils.dart'; + +@internal +class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { + final SentryFlutterOptions options; + late final _native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); + + SentryNative(this.options); + + Future init(SentryFlutterOptions options) async { + assert(this.options == options); + + if (!options.enableNativeCrashHandling) { + options.logger( + SentryLevel.info, 'SentryNative crash handling is disabled'); + return; + } + + final c = FreeableFactory(); + try { + final cOptions = _native.options_new(); + _native.options_set_dsn(cOptions, c.str(options.dsn)); + _native.options_set_debug(cOptions, options.debug ? 1 : 0); + _native.options_set_environment(cOptions, c.str(options.environment)); + _native.options_set_release(cOptions, c.str(options.release)); + _native.options_set_auto_session_tracking( + cOptions, options.enableAutoSessionTracking ? 1 : 0); + _native.options_set_dist(cOptions, c.str(options.dist)); + _native.options_set_max_breadcrumbs(cOptions, options.maxBreadcrumbs); + if (options.proxy != null) { + // sentry-native expects a single string and it doesn't support different types or authentication + options.logger(SentryLevel.warning, + 'SentryNative: setting a proxy is currently not supported'); + } + _native.init(nullptr); + } finally { + c.freeAll(); + } + } + + Future close() { + throw UnimplementedError(); + } + + Future fetchNativeAppStart() { + throw UnimplementedError(); + } + + Future captureEnvelope( + Uint8List envelopeData, bool containsUnhandledException) { + throw UnimplementedError(); + } + + Future beginNativeFrames() { + throw UnimplementedError(); + } + + Future endNativeFrames(SentryId id) { + throw UnimplementedError(); + } + + Future setUser(SentryUser? user) { + throw UnimplementedError(); + } + + Future addBreadcrumb(Breadcrumb breadcrumb) { + throw UnimplementedError(); + } + + Future clearBreadcrumbs() { + throw UnimplementedError(); + } + + Future?> loadContexts() { + throw UnimplementedError(); + } + + Future setContexts(String key, dynamic value) { + throw UnimplementedError(); + } + + Future removeContexts(String key) { + throw UnimplementedError(); + } + + Future setExtra(String key, dynamic value) { + throw UnimplementedError(); + } + + Future removeExtra(String key) { + throw UnimplementedError(); + } + + Future setTag(String key, String value) { + throw UnimplementedError(); + } + + Future removeTag(String key) { + throw UnimplementedError(); + } + + int? startProfiler(SentryId traceId) { + throw UnimplementedError(); + } + + Future discardProfiler(SentryId traceId) { + throw UnimplementedError(); + } + + Future displayRefreshRate() { + throw UnimplementedError(); + } + + Future?> collectProfile( + SentryId traceId, int startTimeNs, int endTimeNs) { + throw UnimplementedError(); + } + + Future?> loadDebugImages() { + throw UnimplementedError(); + } + + Future pauseAppHangTracking() { + throw UnimplementedError(); + } + + Future resumeAppHangTracking() { + throw UnimplementedError(); + } +} diff --git a/flutter/lib/src/native/c/utils.dart b/flutter/lib/src/native/c/utils.dart new file mode 100644 index 0000000000..b3f3ef5e4b --- /dev/null +++ b/flutter/lib/src/native/c/utils.dart @@ -0,0 +1,24 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +/// Creates and collects native pointers that need to be freed. +class FreeableFactory { + List _allocated = []; + + Pointer str(String? dartString) { + if (dartString == null) { + return nullptr; + } + final ptr = dartString.toNativeUtf8(); + _allocated.add(ptr); + return ptr.cast(); + } + + void freeAll() { + for (final ptr in _allocated) { + malloc.free(ptr); + } + _allocated.clear(); + } +} diff --git a/flutter/lib/src/native/factory_real.dart b/flutter/lib/src/native/factory_real.dart index 554279a8bc..130cd1fb72 100644 --- a/flutter/lib/src/native/factory_real.dart +++ b/flutter/lib/src/native/factory_real.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import '../../sentry_flutter.dart'; +import 'c/sentry_native.dart'; import 'cocoa/sentry_native_cocoa.dart'; import 'java/sentry_native_java.dart'; import 'sentry_native_binding.dart'; @@ -13,6 +14,8 @@ SentryNativeBinding createBinding(SentryFlutterOptions options, return SentryNativeCocoa(options, channel); } else if (platform.isAndroid) { return SentryNativeJava(options, channel); + } else if (platform.isWindows) { + return SentryNative(options); } else { return SentryNativeChannel(options, channel); } diff --git a/flutter/scripts/generate-native-bindings.ps1 b/flutter/scripts/generate-native-bindings.ps1 index 7ebab9b597..67d84de610 100644 --- a/flutter/scripts/generate-native-bindings.ps1 +++ b/flutter/scripts/generate-native-bindings.ps1 @@ -13,6 +13,7 @@ try $content = $content -replace 'final class', 'class' $content | Set-Content -NoNewline -Encoding utf8 $binding dart format $binding + Get-Item $binding } finally { From 588c04740c768fd92d7356a6007c75df762664e3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Aug 2024 15:58:17 +0200 Subject: [PATCH 04/55] basic tests --- flutter/ffi-native.yaml | 9 + flutter/lib/src/native/c/binding.dart | 125 +++++++ flutter/lib/src/native/c/sentry_native.dart | 34 +- flutter/sentry-native/sentry-native.cmake | 2 +- .../sentry_native/sentry_native_test.dart | 339 ++++++++++++++++++ 5 files changed, 498 insertions(+), 11 deletions(-) create mode 100644 flutter/test/sentry_native/sentry_native_test.dart diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index f4aa63ef69..c9c936e96a 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -17,6 +17,15 @@ functions: - sentry_options_set_auto_session_tracking - sentry_options_set_dist - sentry_options_set_max_breadcrumbs + # For tests only: + - sentry_options_free + - sentry_options_get_dsn + - sentry_options_get_debug + - sentry_options_get_environment + - sentry_options_get_release + - sentry_options_get_auto_session_tracking + - sentry_options_get_dist + - sentry_options_get_max_breadcrumbs rename: 'sentry_(.*)': '$1' structs: diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index 7c3f439670..b542374030 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -32,6 +32,21 @@ class SentryNative { late final _options_new = _options_newPtr.asFunction Function()>(); + /// Deallocates previously allocated sentry options. + void options_free( + ffi.Pointer opts, + ) { + return _options_free( + opts, + ); + } + + late final _options_freePtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_free'); + late final _options_free = _options_freePtr + .asFunction)>(); + /// Sets the DSN. void options_set_dsn( ffi.Pointer opts, @@ -50,6 +65,22 @@ class SentryNative { late final _options_set_dsn = _options_set_dsnPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer)>(); + /// Gets the DSN. + ffi.Pointer options_get_dsn( + ffi.Pointer opts, + ) { + return _options_get_dsn( + opts, + ); + } + + late final _options_get_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dsn'); + late final _options_get_dsn = _options_get_dsnPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + /// Sets the release. void options_set_release( ffi.Pointer opts, @@ -68,6 +99,22 @@ class SentryNative { late final _options_set_release = _options_set_releasePtr.asFunction< void Function(ffi.Pointer, ffi.Pointer)>(); + /// Gets the release. + ffi.Pointer options_get_release( + ffi.Pointer opts, + ) { + return _options_get_release( + opts, + ); + } + + late final _options_get_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_release'); + late final _options_get_release = _options_get_releasePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + /// Sets the environment. void options_set_environment( ffi.Pointer opts, @@ -86,6 +133,22 @@ class SentryNative { late final _options_set_environment = _options_set_environmentPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer)>(); + /// Gets the environment. + ffi.Pointer options_get_environment( + ffi.Pointer opts, + ) { + return _options_get_environment( + opts, + ); + } + + late final _options_get_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>( + 'sentry_options_get_environment'); + late final _options_get_environment = _options_get_environmentPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + /// Sets the dist. void options_set_dist( ffi.Pointer opts, @@ -104,6 +167,22 @@ class SentryNative { late final _options_set_dist = _options_set_distPtr.asFunction< void Function(ffi.Pointer, ffi.Pointer)>(); + /// Gets the dist. + ffi.Pointer options_get_dist( + ffi.Pointer opts, + ) { + return _options_get_dist( + opts, + ); + } + + late final _options_get_distPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dist'); + late final _options_get_dist = _options_get_distPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + /// Enables or disables debug printing mode. void options_set_debug( ffi.Pointer opts, @@ -122,6 +201,21 @@ class SentryNative { late final _options_set_debug = _options_set_debugPtr .asFunction, int)>(); + /// Returns the current value of the debug flag. + int options_get_debug( + ffi.Pointer opts, + ) { + return _options_get_debug( + opts, + ); + } + + late final _options_get_debugPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_debug'); + late final _options_get_debug = _options_get_debugPtr + .asFunction)>(); + /// Sets the number of breadcrumbs being tracked and attached to events. /// /// Defaults to 100. @@ -142,6 +236,21 @@ class SentryNative { late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr .asFunction, int)>(); + /// Gets the number of breadcrumbs being tracked and attached to events. + int options_get_max_breadcrumbs( + ffi.Pointer opts, + ) { + return _options_get_max_breadcrumbs( + opts, + ); + } + + late final _options_get_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_max_breadcrumbs'); + late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr + .asFunction)>(); + /// Enables or disables automatic session tracking. /// /// Automatic session tracking is enabled by default and is equivalent to calling @@ -167,6 +276,22 @@ class SentryNative { _options_set_auto_session_trackingPtr .asFunction, int)>(); + /// Returns true if automatic session tracking is enabled. + int options_get_auto_session_tracking( + ffi.Pointer opts, + ) { + return _options_get_auto_session_tracking( + opts, + ); + } + + late final _options_get_auto_session_trackingPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_auto_session_tracking'); + late final _options_get_auto_session_tracking = + _options_get_auto_session_trackingPtr + .asFunction)>(); + /// Initializes the Sentry SDK with the specified options. /// /// This takes ownership of the options. After the options have been set diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 354549228d..71e004ecb5 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -14,7 +14,9 @@ import 'utils.dart'; @internal class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { final SentryFlutterOptions options; - late final _native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); + + @visibleForTesting + late final native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); SentryNative(this.options); @@ -27,23 +29,35 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return; } + tryCatchSync("init", () { + final cOptions = createOptions(options); + final code = native.init(cOptions); + if (code != 0) { + throw StateError( + "Failed to initialize native SDK - init() exit code: $code"); + } + }); + } + + Pointer createOptions( + SentryFlutterOptions options) { final c = FreeableFactory(); try { - final cOptions = _native.options_new(); - _native.options_set_dsn(cOptions, c.str(options.dsn)); - _native.options_set_debug(cOptions, options.debug ? 1 : 0); - _native.options_set_environment(cOptions, c.str(options.environment)); - _native.options_set_release(cOptions, c.str(options.release)); - _native.options_set_auto_session_tracking( + final cOptions = native.options_new(); + native.options_set_dsn(cOptions, c.str(options.dsn)); + native.options_set_debug(cOptions, options.debug ? 1 : 0); + native.options_set_environment(cOptions, c.str(options.environment)); + native.options_set_release(cOptions, c.str(options.release)); + native.options_set_auto_session_tracking( cOptions, options.enableAutoSessionTracking ? 1 : 0); - _native.options_set_dist(cOptions, c.str(options.dist)); - _native.options_set_max_breadcrumbs(cOptions, options.maxBreadcrumbs); + native.options_set_dist(cOptions, c.str(options.dist)); + native.options_set_max_breadcrumbs(cOptions, options.maxBreadcrumbs); if (options.proxy != null) { // sentry-native expects a single string and it doesn't support different types or authentication options.logger(SentryLevel.warning, 'SentryNative: setting a proxy is currently not supported'); } - _native.init(nullptr); + return cOptions; } finally { c.freeAll(); } diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake index 6a8886bdf8..1882509324 100644 --- a/flutter/sentry-native/sentry-native.cmake +++ b/flutter/sentry-native/sentry-native.cmake @@ -1,5 +1,5 @@ load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo version) -message(STATUS "Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") +message(STATUS "Fetching Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") set(SENTRY_SDK_NAME "sentry.native.flutter" CACHE STRING "The SDK name to report when sending events." FORCE) set(SENTRY_BACKEND "inproc" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart new file mode 100644 index 0000000000..0f94f80bec --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -0,0 +1,339 @@ +@TestOn('vm && windows') +library flutter_test; + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/src/platform/platform.dart' as platform; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/c/sentry_native.dart'; +import 'package:sentry_flutter/src/native/factory.dart'; + +import '../mocks.dart'; + +/// Runs [command] with command's stdout and stderr being forwrarded to +/// test runner's respective streams. It buffers stdout and returns it. +/// +/// Returns [_CommandResult] with exitCode and stdout as a single sting +Future _exec(String executable, List arguments) async { + final process = await Process.start(executable, arguments); + + // forward standard streams + unawaited(stderr.addStream(process.stderr)); + unawaited(stdout.addStream(process.stdout)); + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "$executable ${arguments.join(' ')} failed with exit code $exitCode"); + } +} + +void main() { + const nativeTestRoot = 'temp/native-test'; + if (Directory.current.path.endsWith('/test')) { + Directory.current = Directory.current.parent; + } + + setUpAll(() async { + // Compile sentry-native using CMake, as if it was part of a Flutter app. + final cmakeBuildDir = '$nativeTestRoot/build'; + final cmakeConfDir = '$nativeTestRoot/conf'; + final cmakeOutputDir = '$nativeTestRoot/out'; + Directory(cmakeConfDir).createSync(recursive: true); + File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' +cmake_minimum_required(VERSION 3.14) +project(sentry-native-flutter-test) +add_subdirectory(../../../${platform.instance.operatingSystem} plugin) +add_library(\${CMAKE_PROJECT_NAME} INTERFACE) +target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled_libraries}) +'''); + await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); + await _exec('cmake', ['--build', cmakeBuildDir]); + Directory.current = '$cmakeBuildDir/_deps/sentry-native-build/Debug/'; + }); + + late SentryNative sut; + late SentryFlutterOptions options; + + setUp(() { + options = SentryFlutterOptions(dsn: fakeDsn) + // ignore: invalid_use_of_internal_member + ..automatedTestMode = true; + sut = createBinding(options) as SentryNative; + }); + + test('options', () { + options + ..debug = true + ..environment = 'foo' + ..release = 'foo@bar+1' + ..enableAutoSessionTracking = true + ..dist = 'distfoo' + ..diagnosticLevel = SentryLevel.error + ..maxBreadcrumbs = 42; + + final cOptions = sut.createOptions(options); + try { + // sut.native.options_get_dsn(cOptions) + } finally { + sut.native.options_free(cOptions); + } + }); + + test('init', () async { + // There's nothing we can check here - just that it doesn't crash. + await sut.init(options); + }); + + // test('beginNativeFrames', () async { + // when(channel.invokeMethod('beginNativeFrames')) + // .thenAnswer((realInvocation) async {}); + // await sut.beginNativeFrames(); + + // verify(channel.invokeMethod('beginNativeFrames')); + // }); + + // test('endNativeFrames', () async { + // final sentryId = SentryId.empty(); + + // when(channel + // .invokeMethod('endNativeFrames', {'id': sentryId.toString()})) + // .thenAnswer((_) async => { + // 'totalFrames': 3, + // 'slowFrames': 2, + // 'frozenFrames': 1, + // }); + + // final actual = await sut.endNativeFrames(sentryId); + + // expect(actual?.totalFrames, 3); + // expect(actual?.slowFrames, 2); + // expect(actual?.frozenFrames, 1); + // }); + + // test('setUser', () async { + // final user = SentryUser( + // id: "fixture-id", + // data: {'object': Object()}, + // ); + // final normalizedUser = user.copyWith( + // data: MethodChannelHelper.normalizeMap(user.data), + // ); + // when(channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})) + // .thenAnswer((_) => Future.value()); + + // await sut.setUser(user); + + // verify( + // channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})); + // }); + + // test('addBreadcrumb', () async { + // final breadcrumb = Breadcrumb( + // data: {'object': Object()}, + // ); + // final normalizedBreadcrumb = breadcrumb.copyWith( + // data: MethodChannelHelper.normalizeMap(breadcrumb.data)); + + // when(channel.invokeMethod( + // 'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()})) + // .thenAnswer((_) => Future.value()); + + // await sut.addBreadcrumb(breadcrumb); + + // verify(channel.invokeMethod( + // 'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()})); + // }); + + // test('clearBreadcrumbs', () async { + // when(channel.invokeMethod('clearBreadcrumbs')) + // .thenAnswer((_) => Future.value()); + + // await sut.clearBreadcrumbs(); + + // verify(channel.invokeMethod('clearBreadcrumbs')); + // }); + + // test('setContexts', () async { + // final value = {'object': Object()}; + // final normalizedValue = MethodChannelHelper.normalize(value); + // when(channel.invokeMethod('setContexts', { + // 'key': 'fixture-key', + // 'value': normalizedValue + // })).thenAnswer((_) => Future.value()); + + // await sut.setContexts('fixture-key', value); + + // verify(channel.invokeMethod( + // 'setContexts', {'key': 'fixture-key', 'value': normalizedValue})); + // }); + + // test('removeContexts', () async { + // when(channel.invokeMethod('removeContexts', {'key': 'fixture-key'})) + // .thenAnswer((_) => Future.value()); + + // await sut.removeContexts('fixture-key'); + + // verify(channel.invokeMethod('removeContexts', {'key': 'fixture-key'})); + // }); + + // test('setExtra', () async { + // final value = {'object': Object()}; + // final normalizedValue = MethodChannelHelper.normalize(value); + // when(channel.invokeMethod( + // 'setExtra', {'key': 'fixture-key', 'value': normalizedValue})) + // .thenAnswer((_) => Future.value()); + + // await sut.setExtra('fixture-key', value); + + // verify(channel.invokeMethod( + // 'setExtra', {'key': 'fixture-key', 'value': normalizedValue})); + // }); + + // test('removeExtra', () async { + // when(channel.invokeMethod('removeExtra', {'key': 'fixture-key'})) + // .thenAnswer((_) => Future.value()); + + // await sut.removeExtra('fixture-key'); + + // verify(channel.invokeMethod('removeExtra', {'key': 'fixture-key'})); + // }); + + // test('setTag', () async { + // when(channel.invokeMethod( + // 'setTag', {'key': 'fixture-key', 'value': 'fixture-value'})) + // .thenAnswer((_) => Future.value()); + + // await sut.setTag('fixture-key', 'fixture-value'); + + // verify(channel.invokeMethod( + // 'setTag', {'key': 'fixture-key', 'value': 'fixture-value'})); + // }); + + // test('removeTag', () async { + // when(channel.invokeMethod('removeTag', {'key': 'fixture-key'})) + // .thenAnswer((_) => Future.value()); + + // await sut.removeTag('fixture-key'); + + // verify(channel.invokeMethod('removeTag', {'key': 'fixture-key'})); + // }); + + // test('startProfiler', () { + // late Matcher matcher; + // if (mockPlatform.isAndroid) { + // matcher = throwsUnsupportedError; + // } else if (mockPlatform.isIOS || mockPlatform.isMacOS) { + // if (platform.instance.isMacOS) { + // matcher = throwsA(predicate((e) => + // e is Exception && + // e.toString().contains('Failed to load Objective-C class'))); + // } else { + // matcher = throwsA(predicate((e) => + // e is ArgumentError && + // e.toString().contains('Failed to lookup symbol'))); + // } + // } + // expect(() => sut.startProfiler(SentryId.newId()), matcher); + + // verifyZeroInteractions(channel); + // }); + + // test('discardProfiler', () async { + // final traceId = SentryId.newId(); + // when(channel.invokeMethod('discardProfiler', traceId.toString())) + // .thenAnswer((_) async {}); + + // await sut.discardProfiler(traceId); + + // verify(channel.invokeMethod('discardProfiler', traceId.toString())); + // }); + + // test('collectProfile', () async { + // final traceId = SentryId.newId(); + // const startTime = 42; + // const endTime = 50; + // when(channel.invokeMethod('collectProfile', { + // 'traceId': traceId.toString(), + // 'startTime': startTime, + // 'endTime': endTime, + // })).thenAnswer((_) async => {}); + + // await sut.collectProfile(traceId, startTime, endTime); + + // verify(channel.invokeMethod('collectProfile', { + // 'traceId': traceId.toString(), + // 'startTime': startTime, + // 'endTime': endTime, + // })); + // }); + + // test('captureEnvelope', () async { + // final data = Uint8List.fromList([1, 2, 3]); + + // late Uint8List captured; + // when(channel.invokeMethod('captureEnvelope', any)).thenAnswer( + // (invocation) async => + // {captured = invocation.positionalArguments[1][0] as Uint8List}); + + // await sut.captureEnvelope(data, false); + + // expect(captured, data); + // }); + + // test('loadContexts', () async { + // when(channel.invokeMethod('loadContexts')) + // .thenAnswer((invocation) async => { + // 'foo': [1, 2, 3], + // 'bar': {'a': 'b'}, + // }); + + // final data = await sut.loadContexts(); + + // expect(data, { + // 'foo': [1, 2, 3], + // 'bar': {'a': 'b'}, + // }); + // }); + + // test('loadDebugImages', () async { + // final json = [ + // { + // 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', + // 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', + // 'image_addr': '0x6f80b000', + // 'image_size': 3092480, + // 'type': 'elf', + // 'debug_id': 'e77c5713-5311-28c2-ecf0-eb73fc39f450', + // 'debug_file': 'test' + // } + // ]; + + // when(channel.invokeMethod('loadImageList')) + // .thenAnswer((invocation) async => json); + + // final data = await sut.loadDebugImages(); + + // expect(data?.map((v) => v.toJson()), json); + // }); + + // test('pauseAppHangTracking', () async { + // when(channel.invokeMethod('pauseAppHangTracking')) + // .thenAnswer((_) => Future.value()); + + // await sut.pauseAppHangTracking(); + + // verify(channel.invokeMethod('pauseAppHangTracking')); + // }); + + // test('resumeAppHangTracking', () async { + // when(channel.invokeMethod('resumeAppHangTracking')) + // .thenAnswer((_) => Future.value()); + + // await sut.resumeAppHangTracking(); + + // verify(channel.invokeMethod('resumeAppHangTracking')); + // }); +} From 4fea4b99c4998d6a012c3e61563cb8055a70a65d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 22 Aug 2024 20:35:48 +0200 Subject: [PATCH 05/55] more native binding functions --- flutter/ffi-native.yaml | 1 + flutter/lib/src/native/c/binding.dart | 4995 ++++++++++++++++- flutter/lib/src/native/c/sentry_native.dart | 112 +- .../lib/src/native/sentry_native_binding.dart | 44 +- flutter/lib/src/sentry_flutter.dart | 12 +- .../sentry_native/sentry_native_test.dart | 103 +- 6 files changed, 4926 insertions(+), 341 deletions(-) diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index c9c936e96a..33f127fa58 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -8,6 +8,7 @@ headers: exclude-all-by-default: true functions: include: + - sentry_.* - sentry_init - sentry_options_new - sentry_options_set_dsn diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index b542374030..4fe981a43b 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -20,301 +20,4882 @@ class SentryNative { lookup) : _lookup = lookup; - /// Creates a new options struct. - /// Can be freed with `sentry_options_free`. - ffi.Pointer options_new() { - return _options_new(); + /// Allocates memory with the underlying allocator. + ffi.Pointer malloc( + int size, + ) { + return _malloc( + size, + ); } - late final _options_newPtr = - _lookup Function()>>( - 'sentry_options_new'); - late final _options_new = - _options_newPtr.asFunction Function()>(); + late final _mallocPtr = + _lookup Function(ffi.Size)>>( + 'sentry_malloc'); + late final _malloc = + _mallocPtr.asFunction Function(int)>(); - /// Deallocates previously allocated sentry options. - void options_free( - ffi.Pointer opts, + /// Releases memory allocated from the underlying allocator. + void free( + ffi.Pointer ptr, ) { - return _options_free( - opts, + return _free( + ptr, ); } - late final _options_freePtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_free'); - late final _options_free = _options_freePtr - .asFunction)>(); + late final _freePtr = + _lookup)>>( + 'sentry_free'); + late final _free = + _freePtr.asFunction)>(); - /// Sets the DSN. - void options_set_dsn( - ffi.Pointer opts, - ffi.Pointer dsn, + /// Increments the reference count on the value. + void value_incref( + sentry_value_u value, ) { - return _options_set_dsn( - opts, - dsn, + return _value_incref( + value, ); } - late final _options_set_dsnPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_dsn'); - late final _options_set_dsn = _options_set_dsnPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); + late final _value_increfPtr = + _lookup>( + 'sentry_value_incref'); + late final _value_incref = + _value_increfPtr.asFunction(); - /// Gets the DSN. - ffi.Pointer options_get_dsn( - ffi.Pointer opts, + /// Decrements the reference count on the value. + void value_decref( + sentry_value_u value, ) { - return _options_get_dsn( - opts, + return _value_decref( + value, ); } - late final _options_get_dsnPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_dsn'); - late final _options_get_dsn = _options_get_dsnPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); + late final _value_decrefPtr = + _lookup>( + 'sentry_value_decref'); + late final _value_decref = + _value_decrefPtr.asFunction(); - /// Sets the release. - void options_set_release( - ffi.Pointer opts, - ffi.Pointer release, + /// Returns the refcount of a value. + int value_refcount( + sentry_value_u value, ) { - return _options_set_release( - opts, - release, + return _value_refcount( + value, ); } - late final _options_set_releasePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_release'); - late final _options_set_release = _options_set_releasePtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); + late final _value_refcountPtr = + _lookup>( + 'sentry_value_refcount'); + late final _value_refcount = + _value_refcountPtr.asFunction(); - /// Gets the release. - ffi.Pointer options_get_release( - ffi.Pointer opts, + /// Freezes a value. + void value_freeze( + sentry_value_u value, ) { - return _options_get_release( - opts, + return _value_freeze( + value, ); } - late final _options_get_releasePtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_release'); - late final _options_get_release = _options_get_releasePtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); + late final _value_freezePtr = + _lookup>( + 'sentry_value_freeze'); + late final _value_freeze = + _value_freezePtr.asFunction(); - /// Sets the environment. - void options_set_environment( - ffi.Pointer opts, - ffi.Pointer environment, + /// Checks if a value is frozen. + int value_is_frozen( + sentry_value_u value, ) { - return _options_set_environment( - opts, - environment, + return _value_is_frozen( + value, ); } - late final _options_set_environmentPtr = _lookup< + late final _value_is_frozenPtr = + _lookup>( + 'sentry_value_is_frozen'); + late final _value_is_frozen = + _value_is_frozenPtr.asFunction(); + + /// Creates a null value. + sentry_value_u value_new_null() { + return _value_new_null(); + } + + late final _value_new_nullPtr = + _lookup>( + 'sentry_value_new_null'); + late final _value_new_null = + _value_new_nullPtr.asFunction(); + + /// Creates a new 32-bit signed integer value. + sentry_value_u value_new_int32( + int value, + ) { + return _value_new_int32( + value, + ); + } + + late final _value_new_int32Ptr = + _lookup>( + 'sentry_value_new_int32'); + late final _value_new_int32 = + _value_new_int32Ptr.asFunction(); + + /// Creates a new double value. + sentry_value_u value_new_double( + double value, + ) { + return _value_new_double( + value, + ); + } + + late final _value_new_doublePtr = + _lookup>( + 'sentry_value_new_double'); + late final _value_new_double = + _value_new_doublePtr.asFunction(); + + /// Creates a new boolean value. + sentry_value_u value_new_bool( + int value, + ) { + return _value_new_bool( + value, + ); + } + + late final _value_new_boolPtr = + _lookup>( + 'sentry_value_new_bool'); + late final _value_new_bool = + _value_new_boolPtr.asFunction(); + + /// Creates a new null terminated string. + sentry_value_u value_new_string( + ffi.Pointer value, + ) { + return _value_new_string( + value, + ); + } + + late final _value_new_stringPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_value_new_string'); + late final _value_new_string = _value_new_stringPtr + .asFunction)>(); + + sentry_value_u value_new_string_n( + ffi.Pointer value, + int value_len, + ) { + return _value_new_string_n( + value, + value_len, + ); + } + + late final _value_new_string_nPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_environment'); - late final _options_set_environment = _options_set_environmentPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); + sentry_value_u Function( + ffi.Pointer, ffi.Size)>>('sentry_value_new_string_n'); + late final _value_new_string_n = _value_new_string_nPtr + .asFunction, int)>(); - /// Gets the environment. - ffi.Pointer options_get_environment( - ffi.Pointer opts, + /// Creates a new list value. + sentry_value_u value_new_list() { + return _value_new_list(); + } + + late final _value_new_listPtr = + _lookup>( + 'sentry_value_new_list'); + late final _value_new_list = + _value_new_listPtr.asFunction(); + + /// Creates a new object. + sentry_value_u value_new_object() { + return _value_new_object(); + } + + late final _value_new_objectPtr = + _lookup>( + 'sentry_value_new_object'); + late final _value_new_object = + _value_new_objectPtr.asFunction(); + + /// Returns the type of the value passed. + int value_get_type( + sentry_value_u value, ) { - return _options_get_environment( - opts, + return _value_get_type( + value, ); } - late final _options_get_environmentPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer)>>( - 'sentry_options_get_environment'); - late final _options_get_environment = _options_get_environmentPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); + late final _value_get_typePtr = + _lookup>( + 'sentry_value_get_type'); + late final _value_get_type = + _value_get_typePtr.asFunction(); - /// Sets the dist. - void options_set_dist( - ffi.Pointer opts, - ffi.Pointer dist, + /// Sets a key to a value in the map. + /// + /// This moves the ownership of the value into the map. The caller does not + /// have to call `sentry_value_decref` on it. + int value_set_by_key( + sentry_value_u value, + ffi.Pointer k, + sentry_value_u v, ) { - return _options_set_dist( - opts, - dist, + return _value_set_by_key( + value, + k, + v, ); } - late final _options_set_distPtr = _lookup< + late final _value_set_by_keyPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_dist'); - late final _options_set_dist = _options_set_distPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); + ffi.Int Function(sentry_value_u, ffi.Pointer, + sentry_value_u)>>('sentry_value_set_by_key'); + late final _value_set_by_key = _value_set_by_keyPtr.asFunction< + int Function(sentry_value_u, ffi.Pointer, sentry_value_u)>(); - /// Gets the dist. - ffi.Pointer options_get_dist( - ffi.Pointer opts, + int value_set_by_key_n( + sentry_value_u value, + ffi.Pointer k, + int k_len, + sentry_value_u v, ) { - return _options_get_dist( - opts, + return _value_set_by_key_n( + value, + k, + k_len, + v, ); } - late final _options_get_distPtr = _lookup< + late final _value_set_by_key_nPtr = _lookup< ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_dist'); - late final _options_get_dist = _options_get_distPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); + ffi.Int Function(sentry_value_u, ffi.Pointer, ffi.Size, + sentry_value_u)>>('sentry_value_set_by_key_n'); + late final _value_set_by_key_n = _value_set_by_key_nPtr.asFunction< + int Function( + sentry_value_u, ffi.Pointer, int, sentry_value_u)>(); - /// Enables or disables debug printing mode. - void options_set_debug( - ffi.Pointer opts, - int debug, + /// This removes a value from the map by key. + int value_remove_by_key( + sentry_value_u value, + ffi.Pointer k, ) { - return _options_set_debug( - opts, - debug, + return _value_remove_by_key( + value, + k, ); } - late final _options_set_debugPtr = _lookup< + late final _value_remove_by_keyPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_debug'); - late final _options_set_debug = _options_set_debugPtr - .asFunction, int)>(); + ffi.Int Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_remove_by_key'); + late final _value_remove_by_key = _value_remove_by_keyPtr + .asFunction)>(); - /// Returns the current value of the debug flag. - int options_get_debug( - ffi.Pointer opts, + int value_remove_by_key_n( + sentry_value_u value, + ffi.Pointer k, + int k_len, ) { - return _options_get_debug( - opts, + return _value_remove_by_key_n( + value, + k, + k_len, ); } - late final _options_get_debugPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_debug'); - late final _options_get_debug = _options_get_debugPtr - .asFunction)>(); + late final _value_remove_by_key_nPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(sentry_value_u, ffi.Pointer, + ffi.Size)>>('sentry_value_remove_by_key_n'); + late final _value_remove_by_key_n = _value_remove_by_key_nPtr + .asFunction, int)>(); - /// Sets the number of breadcrumbs being tracked and attached to events. + /// Appends a value to a list. /// - /// Defaults to 100. - void options_set_max_breadcrumbs( - ffi.Pointer opts, - int max_breadcrumbs, + /// This moves the ownership of the value into the list. The caller does not + /// have to call `sentry_value_decref` on it. + int value_append( + sentry_value_u value, + sentry_value_u v, ) { - return _options_set_max_breadcrumbs( - opts, - max_breadcrumbs, + return _value_append( + value, + v, ); } - late final _options_set_max_breadcrumbsPtr = _lookup< + late final _value_appendPtr = _lookup< + ffi.NativeFunction>( + 'sentry_value_append'); + late final _value_append = _value_appendPtr + .asFunction(); + + /// Inserts a value into the list at a certain position. + /// + /// This moves the ownership of the value into the list. The caller does not + /// have to call `sentry_value_decref` on it. + /// + /// If the list is shorter than the given index it's automatically extended + /// and filled with `null` values. + int value_set_by_index( + sentry_value_u value, + int index, + sentry_value_u v, + ) { + return _value_set_by_index( + value, + index, + v, + ); + } + + late final _value_set_by_indexPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Size)>>('sentry_options_set_max_breadcrumbs'); - late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr - .asFunction, int)>(); + ffi.Int Function(sentry_value_u, ffi.Size, + sentry_value_u)>>('sentry_value_set_by_index'); + late final _value_set_by_index = _value_set_by_indexPtr + .asFunction(); - /// Gets the number of breadcrumbs being tracked and attached to events. - int options_get_max_breadcrumbs( - ffi.Pointer opts, + /// This removes a value from the list by index. + int value_remove_by_index( + sentry_value_u value, + int index, ) { - return _options_get_max_breadcrumbs( - opts, + return _value_remove_by_index( + value, + index, ); } - late final _options_get_max_breadcrumbsPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_max_breadcrumbs'); - late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr - .asFunction)>(); + late final _value_remove_by_indexPtr = + _lookup>( + 'sentry_value_remove_by_index'); + late final _value_remove_by_index = + _value_remove_by_indexPtr.asFunction(); - /// Enables or disables automatic session tracking. - /// - /// Automatic session tracking is enabled by default and is equivalent to calling - /// `sentry_start_session` after startup. - /// There can only be one running session, and the current session will always be - /// closed implicitly by `sentry_close`, when starting a new session with - /// `sentry_start_session`, or manually by calling `sentry_end_session`. - void options_set_auto_session_tracking( - ffi.Pointer opts, - int val, + /// Looks up a value in a map by key. If missing a null value is returned. + /// The returned value is borrowed. + sentry_value_u value_get_by_key( + sentry_value_u value, + ffi.Pointer k, ) { - return _options_set_auto_session_tracking( - opts, - val, + return _value_get_by_key( + value, + k, ); } - late final _options_set_auto_session_trackingPtr = _lookup< + late final _value_get_by_keyPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_auto_session_tracking'); - late final _options_set_auto_session_tracking = - _options_set_auto_session_trackingPtr - .asFunction, int)>(); + sentry_value_u Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_get_by_key'); + late final _value_get_by_key = _value_get_by_keyPtr.asFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer)>(); - /// Returns true if automatic session tracking is enabled. - int options_get_auto_session_tracking( - ffi.Pointer opts, + sentry_value_u value_get_by_key_n( + sentry_value_u value, + ffi.Pointer k, + int k_len, ) { - return _options_get_auto_session_tracking( - opts, + return _value_get_by_key_n( + value, + k, + k_len, ); } - late final _options_get_auto_session_trackingPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_auto_session_tracking'); - late final _options_get_auto_session_tracking = - _options_get_auto_session_trackingPtr - .asFunction)>(); + late final _value_get_by_key_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer, + ffi.Size)>>('sentry_value_get_by_key_n'); + late final _value_get_by_key_n = _value_get_by_key_nPtr.asFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer, int)>(); - /// Initializes the Sentry SDK with the specified options. + /// Looks up a value in a map by key. If missing a null value is returned. + /// The returned value is owned. /// - /// This takes ownership of the options. After the options have been set - /// they cannot be modified any more. - /// Depending on the configured transport and backend, this function might not be - /// fully thread-safe. - /// Returns 0 on success. - int init( - ffi.Pointer options, + /// If the caller no longer needs the value it must be released with + /// `sentry_value_decref`. + sentry_value_u value_get_by_key_owned( + sentry_value_u value, + ffi.Pointer k, ) { - return _init( - options, + return _value_get_by_key_owned( + value, + k, ); } - late final _initPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_init'); - late final _init = - _initPtr.asFunction)>(); -} + late final _value_get_by_key_ownedPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_get_by_key_owned'); + late final _value_get_by_key_owned = _value_get_by_key_ownedPtr.asFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer)>(); -/// The Sentry Client Options. -/// -/// See https://docs.sentry.io/platforms/native/configuration/ -class sentry_options_s extends ffi.Opaque {} + sentry_value_u value_get_by_key_owned_n( + sentry_value_u value, + ffi.Pointer k, + int k_len, + ) { + return _value_get_by_key_owned_n( + value, + k, + k_len, + ); + } + + late final _value_get_by_key_owned_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer, + ffi.Size)>>('sentry_value_get_by_key_owned_n'); + late final _value_get_by_key_owned_n = + _value_get_by_key_owned_nPtr.asFunction< + sentry_value_u Function( + sentry_value_u, ffi.Pointer, int)>(); + + /// Looks up a value in a list by index. If missing a null value is returned. + /// The returned value is borrowed. + sentry_value_u value_get_by_index( + sentry_value_u value, + int index, + ) { + return _value_get_by_index( + value, + index, + ); + } + + late final _value_get_by_indexPtr = _lookup< + ffi + .NativeFunction>( + 'sentry_value_get_by_index'); + late final _value_get_by_index = _value_get_by_indexPtr + .asFunction(); + + /// Looks up a value in a list by index. If missing a null value is + /// returned. The returned value is owned. + /// + /// If the caller no longer needs the value it must be released with + /// `sentry_value_decref`. + sentry_value_u value_get_by_index_owned( + sentry_value_u value, + int index, + ) { + return _value_get_by_index_owned( + value, + index, + ); + } + + late final _value_get_by_index_ownedPtr = _lookup< + ffi + .NativeFunction>( + 'sentry_value_get_by_index_owned'); + late final _value_get_by_index_owned = _value_get_by_index_ownedPtr + .asFunction(); + + /// Returns the length of the given map or list. + /// + /// If an item is not a list or map the return value is 0. + int value_get_length( + sentry_value_u value, + ) { + return _value_get_length( + value, + ); + } + + late final _value_get_lengthPtr = + _lookup>( + 'sentry_value_get_length'); + late final _value_get_length = + _value_get_lengthPtr.asFunction(); + + /// Converts a value into a 32bit signed integer. + int value_as_int32( + sentry_value_u value, + ) { + return _value_as_int32( + value, + ); + } + + late final _value_as_int32Ptr = + _lookup>( + 'sentry_value_as_int32'); + late final _value_as_int32 = + _value_as_int32Ptr.asFunction(); + + /// Converts a value into a double value. + double value_as_double( + sentry_value_u value, + ) { + return _value_as_double( + value, + ); + } + + late final _value_as_doublePtr = + _lookup>( + 'sentry_value_as_double'); + late final _value_as_double = + _value_as_doublePtr.asFunction(); + + /// Returns the value as c string. + ffi.Pointer value_as_string( + sentry_value_u value, + ) { + return _value_as_string( + value, + ); + } + + late final _value_as_stringPtr = _lookup< + ffi.NativeFunction Function(sentry_value_u)>>( + 'sentry_value_as_string'); + late final _value_as_string = _value_as_stringPtr + .asFunction Function(sentry_value_u)>(); + + /// Returns `true` if the value is boolean true. + int value_is_true( + sentry_value_u value, + ) { + return _value_is_true( + value, + ); + } + + late final _value_is_truePtr = + _lookup>( + 'sentry_value_is_true'); + late final _value_is_true = + _value_is_truePtr.asFunction(); + + /// Returns `true` if the value is null. + int value_is_null( + sentry_value_u value, + ) { + return _value_is_null( + value, + ); + } + + late final _value_is_nullPtr = + _lookup>( + 'sentry_value_is_null'); + late final _value_is_null = + _value_is_nullPtr.asFunction(); + + /// Serialize a sentry value to JSON. + /// + /// The string is freshly allocated and must be freed with + /// `sentry_string_free`. + ffi.Pointer value_to_json( + sentry_value_u value, + ) { + return _value_to_json( + value, + ); + } + + late final _value_to_jsonPtr = _lookup< + ffi.NativeFunction Function(sentry_value_u)>>( + 'sentry_value_to_json'); + late final _value_to_json = _value_to_jsonPtr + .asFunction Function(sentry_value_u)>(); + + /// Creates a new empty Event value. + /// + /// See https://docs.sentry.io/platforms/native/enriching-events/ for how to + /// further work with events, and https://develop.sentry.dev/sdk/event-payloads/ + /// for a detailed overview of the possible properties of an Event. + sentry_value_u value_new_event() { + return _value_new_event(); + } + + late final _value_new_eventPtr = + _lookup>( + 'sentry_value_new_event'); + late final _value_new_event = + _value_new_eventPtr.asFunction(); + + /// Creates a new Message Event value. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/message/ + /// + /// `logger` can be NULL to omit the logger value. + sentry_value_u value_new_message_event( + int level, + ffi.Pointer logger, + ffi.Pointer text, + ) { + return _value_new_message_event( + level, + logger, + text, + ); + } + + late final _value_new_message_eventPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Int32, ffi.Pointer, + ffi.Pointer)>>('sentry_value_new_message_event'); + late final _value_new_message_event = _value_new_message_eventPtr.asFunction< + sentry_value_u Function( + int, ffi.Pointer, ffi.Pointer)>(); + + sentry_value_u value_new_message_event_n( + int level, + ffi.Pointer logger, + int logger_len, + ffi.Pointer text, + int text_len, + ) { + return _value_new_message_event_n( + level, + logger, + logger_len, + text, + text_len, + ); + } + + late final _value_new_message_event_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Int32, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_value_new_message_event_n'); + late final _value_new_message_event_n = + _value_new_message_event_nPtr.asFunction< + sentry_value_u Function( + int, ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Creates a new Breadcrumb with a specific type and message. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ + /// + /// Either parameter can be NULL in which case no such attributes is created. + sentry_value_u value_new_breadcrumb( + ffi.Pointer type, + ffi.Pointer message, + ) { + return _value_new_breadcrumb( + type, + message, + ); + } + + late final _value_new_breadcrumbPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer, + ffi.Pointer)>>('sentry_value_new_breadcrumb'); + late final _value_new_breadcrumb = _value_new_breadcrumbPtr.asFunction< + sentry_value_u Function(ffi.Pointer, ffi.Pointer)>(); + + sentry_value_u value_new_breadcrumb_n( + ffi.Pointer type, + int type_len, + ffi.Pointer message, + int message_len, + ) { + return _value_new_breadcrumb_n( + type, + type_len, + message, + message_len, + ); + } + + late final _value_new_breadcrumb_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_value_new_breadcrumb_n'); + late final _value_new_breadcrumb_n = _value_new_breadcrumb_nPtr.asFunction< + sentry_value_u Function( + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Creates a new Exception value. + /// + /// This is intended for capturing language-level exception, such as from a + /// try-catch block. `type` and `value` here refer to the exception class and + /// a possible description. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/exception/ + /// + /// The returned value needs to be attached to an event via + /// `sentry_event_add_exception`. + sentry_value_u value_new_exception( + ffi.Pointer type, + ffi.Pointer value, + ) { + return _value_new_exception( + type, + value, + ); + } + + late final _value_new_exceptionPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer, + ffi.Pointer)>>('sentry_value_new_exception'); + late final _value_new_exception = _value_new_exceptionPtr.asFunction< + sentry_value_u Function(ffi.Pointer, ffi.Pointer)>(); + + sentry_value_u value_new_exception_n( + ffi.Pointer type, + int type_len, + ffi.Pointer value, + int value_len, + ) { + return _value_new_exception_n( + type, + type_len, + value, + value_len, + ); + } + + late final _value_new_exception_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_value_new_exception_n'); + late final _value_new_exception_n = _value_new_exception_nPtr.asFunction< + sentry_value_u Function( + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Creates a new Thread value. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/threads/ + /// + /// The returned value needs to be attached to an event via + /// `sentry_event_add_thread`. + /// + /// `name` can be NULL. + sentry_value_u value_new_thread( + int id, + ffi.Pointer name, + ) { + return _value_new_thread( + id, + name, + ); + } + + late final _value_new_threadPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Uint64, ffi.Pointer)>>('sentry_value_new_thread'); + late final _value_new_thread = _value_new_threadPtr + .asFunction)>(); + + sentry_value_u value_new_thread_n( + int id, + ffi.Pointer name, + int name_len, + ) { + return _value_new_thread_n( + id, + name, + name_len, + ); + } + + late final _value_new_thread_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Uint64, ffi.Pointer, + ffi.Size)>>('sentry_value_new_thread_n'); + late final _value_new_thread_n = _value_new_thread_nPtr + .asFunction, int)>(); + + /// Creates a new Stack Trace conforming to the Stack Trace Interface. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/stacktrace/ + /// + /// The returned object must be attached to either an exception or thread + /// object. + /// + /// If `ips` is NULL the current stack trace is captured, otherwise `len` + /// stack trace instruction pointers are attached to the event. + sentry_value_u value_new_stacktrace( + ffi.Pointer> ips, + int len, + ) { + return _value_new_stacktrace( + ips, + len, + ); + } + + late final _value_new_stacktracePtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer>, + ffi.Size)>>('sentry_value_new_stacktrace'); + late final _value_new_stacktrace = _value_new_stacktracePtr.asFunction< + sentry_value_u Function(ffi.Pointer>, int)>(); + + /// Sets the Stack Trace conforming to the Stack Trace Interface in a value. + /// + /// The value argument must be either an exception or thread object. + /// + /// If `ips` is NULL the current stack trace is captured, otherwise `len` stack + /// trace instruction pointers are attached to the event. + void value_set_stacktrace( + sentry_value_u value, + ffi.Pointer> ips, + int len, + ) { + return _value_set_stacktrace( + value, + ips, + len, + ); + } + + late final _value_set_stacktracePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(sentry_value_u, ffi.Pointer>, + ffi.Size)>>('sentry_value_set_stacktrace'); + late final _value_set_stacktrace = _value_set_stacktracePtr.asFunction< + void Function(sentry_value_u, ffi.Pointer>, int)>(); + + /// Adds an Exception to an Event value. + /// + /// This takes ownership of the `exception`. + void event_add_exception( + sentry_value_u event, + sentry_value_u exception, + ) { + return _event_add_exception( + event, + exception, + ); + } + + late final _event_add_exceptionPtr = _lookup< + ffi + .NativeFunction>( + 'sentry_event_add_exception'); + late final _event_add_exception = _event_add_exceptionPtr + .asFunction(); + + /// Adds a Thread to an Event value. + /// + /// This takes ownership of the `thread`. + void event_add_thread( + sentry_value_u event, + sentry_value_u thread, + ) { + return _event_add_thread( + event, + thread, + ); + } + + late final _event_add_threadPtr = _lookup< + ffi + .NativeFunction>( + 'sentry_event_add_thread'); + late final _event_add_thread = _event_add_threadPtr + .asFunction(); + + /// Serialize a sentry value to msgpack. + /// + /// The string is freshly allocated and must be freed with + /// `sentry_string_free`. Since msgpack is not zero terminated + /// the size is written to the `size_out` parameter. + ffi.Pointer value_to_msgpack( + sentry_value_u value, + ffi.Pointer size_out, + ) { + return _value_to_msgpack( + value, + size_out, + ); + } + + late final _value_to_msgpackPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(sentry_value_u, + ffi.Pointer)>>('sentry_value_to_msgpack'); + late final _value_to_msgpack = _value_to_msgpackPtr.asFunction< + ffi.Pointer Function(sentry_value_u, ffi.Pointer)>(); + + /// Adds a stack trace to an event. + /// + /// The stack trace is added as part of a new thread object. + /// This function is **deprecated** in favor of using + /// `sentry_value_new_stacktrace` in combination with `sentry_value_new_thread` + /// and `sentry_event_add_thread`. + /// + /// If `ips` is NULL the current stack trace is captured, otherwise `len` + /// stack trace instruction pointers are attached to the event. + void event_value_add_stacktrace( + sentry_value_u event, + ffi.Pointer> ips, + int len, + ) { + return _event_value_add_stacktrace( + event, + ips, + len, + ); + } + + late final _event_value_add_stacktracePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(sentry_value_u, ffi.Pointer>, + ffi.Size)>>('sentry_event_value_add_stacktrace'); + late final _event_value_add_stacktrace = + _event_value_add_stacktracePtr.asFunction< + void Function( + sentry_value_u, ffi.Pointer>, int)>(); + + /// Unwinds the stack from the given address. + /// + /// If the address is given in `addr` the stack is unwound form there. + /// Otherwise (NULL is passed) the current instruction pointer is used as + /// start address. + /// Unwinding with a given `addr` is not supported on all platforms. + /// + /// The stack trace in the form of instruction-addresses, is written to the + /// caller allocated `stacktrace_out`, with up to `max_len` frames being written. + /// The actual number of unwound stackframes is returned. + int unwind_stack( + ffi.Pointer addr, + ffi.Pointer> stacktrace_out, + int max_len, + ) { + return _unwind_stack( + addr, + stacktrace_out, + max_len, + ); + } + + late final _unwind_stackPtr = _lookup< + ffi.NativeFunction< + ffi.Size Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Size)>>('sentry_unwind_stack'); + late final _unwind_stack = _unwind_stackPtr.asFunction< + int Function( + ffi.Pointer, ffi.Pointer>, int)>(); + + /// Unwinds the stack from the given context. + /// + /// The caller is responsible to construct an appropriate `sentry_ucontext_t`. + /// Unwinding from a user context is not supported on all platforms. + /// + /// The stack trace in the form of instruction-addresses, is written to the + /// caller allocated `stacktrace_out`, with up to `max_len` frames being written. + /// The actual number of unwound stackframes is returned. + int unwind_stack_from_ucontext( + ffi.Pointer uctx, + ffi.Pointer> stacktrace_out, + int max_len, + ) { + return _unwind_stack_from_ucontext( + uctx, + stacktrace_out, + max_len, + ); + } + + late final _unwind_stack_from_ucontextPtr = _lookup< + ffi.NativeFunction< + ffi.Size Function( + ffi.Pointer, + ffi.Pointer>, + ffi.Size)>>('sentry_unwind_stack_from_ucontext'); + late final _unwind_stack_from_ucontext = + _unwind_stack_from_ucontextPtr.asFunction< + int Function(ffi.Pointer, + ffi.Pointer>, int)>(); + + /// Creates the nil uuid. + sentry_uuid_s uuid_nil() { + return _uuid_nil(); + } + + late final _uuid_nilPtr = + _lookup>('sentry_uuid_nil'); + late final _uuid_nil = _uuid_nilPtr.asFunction(); + + /// Creates a new uuid4. + sentry_uuid_s uuid_new_v4() { + return _uuid_new_v4(); + } + + late final _uuid_new_v4Ptr = + _lookup>( + 'sentry_uuid_new_v4'); + late final _uuid_new_v4 = + _uuid_new_v4Ptr.asFunction(); + + /// Parses a uuid from a string. + sentry_uuid_s uuid_from_string( + ffi.Pointer str, + ) { + return _uuid_from_string( + str, + ); + } + + late final _uuid_from_stringPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_uuid_from_string'); + late final _uuid_from_string = _uuid_from_stringPtr + .asFunction)>(); + + sentry_uuid_s uuid_from_string_n( + ffi.Pointer str, + int str_len, + ) { + return _uuid_from_string_n( + str, + str_len, + ); + } + + late final _uuid_from_string_nPtr = _lookup< + ffi.NativeFunction< + sentry_uuid_s Function( + ffi.Pointer, ffi.Size)>>('sentry_uuid_from_string_n'); + late final _uuid_from_string_n = _uuid_from_string_nPtr + .asFunction, int)>(); + + /// Creates a uuid from bytes. + sentry_uuid_s uuid_from_bytes( + ffi.Pointer bytes, + ) { + return _uuid_from_bytes( + bytes, + ); + } + + late final _uuid_from_bytesPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_uuid_from_bytes'); + late final _uuid_from_bytes = _uuid_from_bytesPtr + .asFunction)>(); + + /// Checks if the uuid is nil. + int uuid_is_nil( + ffi.Pointer uuid, + ) { + return _uuid_is_nil( + uuid, + ); + } + + late final _uuid_is_nilPtr = + _lookup)>>( + 'sentry_uuid_is_nil'); + late final _uuid_is_nil = + _uuid_is_nilPtr.asFunction)>(); + + /// Returns the bytes of the uuid. + void uuid_as_bytes( + ffi.Pointer uuid, + ffi.Pointer bytes, + ) { + return _uuid_as_bytes( + uuid, + bytes, + ); + } + + late final _uuid_as_bytesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_uuid_as_bytes'); + late final _uuid_as_bytes = _uuid_as_bytesPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Formats the uuid into a string buffer. + void uuid_as_string( + ffi.Pointer uuid, + ffi.Pointer str, + ) { + return _uuid_as_string( + uuid, + str, + ); + } + + late final _uuid_as_stringPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_uuid_as_string'); + late final _uuid_as_string = _uuid_as_stringPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Frees an envelope. + void envelope_free( + ffi.Pointer envelope, + ) { + return _envelope_free( + envelope, + ); + } + + late final _envelope_freePtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_envelope_free'); + late final _envelope_free = _envelope_freePtr + .asFunction)>(); + + /// Given an Envelope, returns the embedded Event if there is one. + /// + /// This returns a borrowed value to the Event in the Envelope. + sentry_value_u envelope_get_event( + ffi.Pointer envelope, + ) { + return _envelope_get_event( + envelope, + ); + } + + late final _envelope_get_eventPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Pointer)>>('sentry_envelope_get_event'); + late final _envelope_get_event = _envelope_get_eventPtr + .asFunction)>(); + + /// Given an Envelope, returns the embedded Transaction if there is one. + /// + /// This returns a borrowed value to the Transaction in the Envelope. + sentry_value_u envelope_get_transaction( + ffi.Pointer envelope, + ) { + return _envelope_get_transaction( + envelope, + ); + } + + late final _envelope_get_transactionPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer)>>( + 'sentry_envelope_get_transaction'); + late final _envelope_get_transaction = _envelope_get_transactionPtr + .asFunction)>(); + + /// Serializes the envelope. + /// + /// The return value needs to be freed with sentry_string_free(). + ffi.Pointer envelope_serialize( + ffi.Pointer envelope, + ffi.Pointer size_out, + ) { + return _envelope_serialize( + envelope, + size_out, + ); + } + + late final _envelope_serializePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer)>>('sentry_envelope_serialize'); + late final _envelope_serialize = _envelope_serializePtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + /// Serializes the envelope into a file. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// + /// Returns 0 on success. + int envelope_write_to_file( + ffi.Pointer envelope, + ffi.Pointer path, + ) { + return _envelope_write_to_file( + envelope, + path, + ); + } + + late final _envelope_write_to_filePtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Pointer)>>('sentry_envelope_write_to_file'); + late final _envelope_write_to_file = _envelope_write_to_filePtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer)>(); + + int envelope_write_to_file_n( + ffi.Pointer envelope, + ffi.Pointer path, + int path_len, + ) { + return _envelope_write_to_file_n( + envelope, + path, + path_len, + ); + } + + late final _envelope_write_to_file_nPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_envelope_write_to_file_n'); + late final _envelope_write_to_file_n = + _envelope_write_to_file_nPtr.asFunction< + int Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Creates a new transport with an initial `send_func`. + ffi.Pointer transport_new( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer envelope, + ffi.Pointer state)>> + send_func, + ) { + return _transport_new( + send_func, + ); + } + + late final _transport_newPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer envelope, + ffi.Pointer state)>>)>>( + 'sentry_transport_new'); + late final _transport_new = _transport_newPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer envelope, + ffi.Pointer state)>>)>(); + + /// Sets the transport `state`. + /// + /// If the state is owned by the transport and needs to be freed, use + /// `sentry_transport_set_free_func` to set an appropriate hook. + void transport_set_state( + ffi.Pointer transport, + ffi.Pointer state, + ) { + return _transport_set_state( + transport, + state, + ); + } + + late final _transport_set_statePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_transport_set_state'); + late final _transport_set_state = _transport_set_statePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + /// Sets the transport hook to free the transport `state`. + void transport_set_free_func( + ffi.Pointer transport, + ffi.Pointer< + ffi.NativeFunction state)>> + free_func, + ) { + return _transport_set_free_func( + transport, + free_func, + ); + } + + late final _transport_set_free_funcPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer state)>>)>>( + 'sentry_transport_set_free_func'); + late final _transport_set_free_func = _transport_set_free_funcPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer state)>>)>(); + + /// Sets the transport startup hook. + /// + /// This hook is called from within `sentry_init` and will get a reference to the + /// options which can be used to initialize a transports internal state. + /// It should return `0` on success. A failure will bubble up to `sentry_init`. + void transport_set_startup_func( + ffi.Pointer transport, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer options, + ffi.Pointer state)>> + startup_func, + ) { + return _transport_set_startup_func( + transport, + startup_func, + ); + } + + late final _transport_set_startup_funcPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Pointer options, + ffi.Pointer state)>>)>>( + 'sentry_transport_set_startup_func'); + late final _transport_set_startup_func = + _transport_set_startup_funcPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer options, + ffi.Pointer state)>>)>(); + + /// Sets the transport flush hook. + /// + /// This hook will receive a millisecond-resolution timeout. + /// It should return `0` if all the pending envelopes are + /// sent within the timeout, or `1` if the timeout is hit. + void transport_set_flush_func( + ffi.Pointer transport, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Uint64 timeout, ffi.Pointer state)>> + flush_func, + ) { + return _transport_set_flush_func( + transport, + flush_func, + ); + } + + late final _transport_set_flush_funcPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Uint64 timeout, + ffi.Pointer state)>>)>>( + 'sentry_transport_set_flush_func'); + late final _transport_set_flush_func = + _transport_set_flush_funcPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Uint64 timeout, + ffi.Pointer state)>>)>(); + + /// Sets the transport shutdown hook. + /// + /// This hook will receive a millisecond-resolution timeout. + /// It should return `0` on success in case all the pending envelopes have been + /// sent within the timeout, or `1` if the timeout was hit. + void transport_set_shutdown_func( + ffi.Pointer transport, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function( + ffi.Uint64 timeout, ffi.Pointer state)>> + shutdown_func, + ) { + return _transport_set_shutdown_func( + transport, + shutdown_func, + ); + } + + late final _transport_set_shutdown_funcPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Uint64 timeout, + ffi.Pointer state)>>)>>( + 'sentry_transport_set_shutdown_func'); + late final _transport_set_shutdown_func = + _transport_set_shutdown_funcPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Int Function(ffi.Uint64 timeout, + ffi.Pointer state)>>)>(); + + /// Generic way to free a transport. + void transport_free( + ffi.Pointer transport, + ) { + return _transport_free( + transport, + ); + } + + late final _transport_freePtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_transport_free'); + late final _transport_free = _transport_freePtr + .asFunction)>(); + + /// Create a new function transport. + /// + /// It is a convenience function which works with a borrowed `data`, and will + /// automatically free the envelope, so the user provided function does not need + /// to do that. + /// + /// This function is *deprecated* and will be removed in a future version. + /// It is here for backwards compatibility. Users should migrate to the + /// `sentry_transport_new` API. + ffi.Pointer new_function_transport( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer envelope, + ffi.Pointer data)>> + func, + ffi.Pointer data, + ) { + return _new_function_transport( + func, + data, + ); + } + + late final _new_function_transportPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer envelope, + ffi.Pointer data)>>, + ffi.Pointer)>>('sentry_new_function_transport'); + late final _new_function_transport = _new_function_transportPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer envelope, + ffi.Pointer data)>>, + ffi.Pointer)>(); + + /// Creates a new options struct. + /// Can be freed with `sentry_options_free`. + ffi.Pointer options_new() { + return _options_new(); + } + + late final _options_newPtr = + _lookup Function()>>( + 'sentry_options_new'); + late final _options_new = + _options_newPtr.asFunction Function()>(); + + /// Deallocates previously allocated sentry options. + void options_free( + ffi.Pointer opts, + ) { + return _options_free( + opts, + ); + } + + late final _options_freePtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_free'); + late final _options_free = _options_freePtr + .asFunction)>(); + + /// Sets a transport. + void options_set_transport( + ffi.Pointer opts, + ffi.Pointer transport, + ) { + return _options_set_transport( + opts, + transport, + ); + } + + late final _options_set_transportPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>( + 'sentry_options_set_transport'); + late final _options_set_transport = _options_set_transportPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + /// Sets the `before_send` callback. + /// + /// See the `sentry_event_function_t` typedef above for more information. + void options_set_before_send( + ffi.Pointer opts, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer, + ffi.Pointer)>> + func, + ffi.Pointer data, + ) { + return _options_set_before_send( + opts, + func, + data, + ); + } + + late final _options_set_before_sendPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>>('sentry_options_set_before_send'); + late final _options_set_before_send = _options_set_before_sendPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(sentry_value_u, ffi.Pointer, + ffi.Pointer)>>, + ffi.Pointer)>(); + + /// Sets the `on_crash` callback. + /// + /// See the `sentry_crash_function_t` typedef above for more information. + void options_set_on_crash( + ffi.Pointer opts, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer, + sentry_value_u, ffi.Pointer)>> + func, + ffi.Pointer data, + ) { + return _options_set_on_crash( + opts, + func, + data, + ); + } + + late final _options_set_on_crashPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer, + sentry_value_u, ffi.Pointer)>>, + ffi.Pointer)>>('sentry_options_set_on_crash'); + late final _options_set_on_crash = _options_set_on_crashPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + sentry_value_u Function(ffi.Pointer, + sentry_value_u, ffi.Pointer)>>, + ffi.Pointer)>(); + + /// Sets the DSN. + void options_set_dsn( + ffi.Pointer opts, + ffi.Pointer dsn, + ) { + return _options_set_dsn( + opts, + dsn, + ); + } + + late final _options_set_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dsn'); + late final _options_set_dsn = _options_set_dsnPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_dsn_n( + ffi.Pointer opts, + ffi.Pointer dsn, + int dsn_len, + ) { + return _options_set_dsn_n( + opts, + dsn, + dsn_len, + ); + } + + late final _options_set_dsn_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Size)>>('sentry_options_set_dsn_n'); + late final _options_set_dsn_n = _options_set_dsn_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Gets the DSN. + ffi.Pointer options_get_dsn( + ffi.Pointer opts, + ) { + return _options_get_dsn( + opts, + ); + } + + late final _options_get_dsnPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dsn'); + late final _options_get_dsn = _options_get_dsnPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the sample rate, which should be a double between `0.0` and `1.0`. + /// Sentry will randomly discard any event that is captured using + /// `sentry_capture_event` when a sample rate < 1 is set. + /// + /// The sampling happens at the end of the event processing according to the + /// following order: + /// + /// https://develop.sentry.dev/sdk/sessions/#filter-order + /// + /// Only items 3. to 6. are currently applicable to sentry-native. This means + /// each processing step is executed even if the sampling discards the event + /// before sending it to the backend. This is particularly relevant to users of + /// the `before_send` callback. + /// + /// The above is in contrast to versions up to 0.4.18 where the sampling happened + /// at the beginning of the processing/filter sequence. + void options_set_sample_rate( + ffi.Pointer opts, + double sample_rate, + ) { + return _options_set_sample_rate( + opts, + sample_rate, + ); + } + + late final _options_set_sample_ratePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Double)>>('sentry_options_set_sample_rate'); + late final _options_set_sample_rate = _options_set_sample_ratePtr + .asFunction, double)>(); + + /// Gets the sample rate. + double options_get_sample_rate( + ffi.Pointer opts, + ) { + return _options_get_sample_rate( + opts, + ); + } + + late final _options_get_sample_ratePtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_options_get_sample_rate'); + late final _options_get_sample_rate = _options_get_sample_ratePtr + .asFunction)>(); + + /// Sets the release. + void options_set_release( + ffi.Pointer opts, + ffi.Pointer release, + ) { + return _options_set_release( + opts, + release, + ); + } + + late final _options_set_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_release'); + late final _options_set_release = _options_set_releasePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_release_n( + ffi.Pointer opts, + ffi.Pointer release, + int release_len, + ) { + return _options_set_release_n( + opts, + release, + release_len, + ); + } + + late final _options_set_release_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_release_n'); + late final _options_set_release_n = _options_set_release_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Gets the release. + ffi.Pointer options_get_release( + ffi.Pointer opts, + ) { + return _options_get_release( + opts, + ); + } + + late final _options_get_releasePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_release'); + late final _options_get_release = _options_get_releasePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the environment. + void options_set_environment( + ffi.Pointer opts, + ffi.Pointer environment, + ) { + return _options_set_environment( + opts, + environment, + ); + } + + late final _options_set_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_environment'); + late final _options_set_environment = _options_set_environmentPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_environment_n( + ffi.Pointer opts, + ffi.Pointer environment, + int environment_len, + ) { + return _options_set_environment_n( + opts, + environment, + environment_len, + ); + } + + late final _options_set_environment_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_environment_n'); + late final _options_set_environment_n = + _options_set_environment_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Gets the environment. + ffi.Pointer options_get_environment( + ffi.Pointer opts, + ) { + return _options_get_environment( + opts, + ); + } + + late final _options_get_environmentPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>( + 'sentry_options_get_environment'); + late final _options_get_environment = _options_get_environmentPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Sets the dist. + void options_set_dist( + ffi.Pointer opts, + ffi.Pointer dist, + ) { + return _options_set_dist( + opts, + dist, + ); + } + + late final _options_set_distPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dist'); + late final _options_set_dist = _options_set_distPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_dist_n( + ffi.Pointer opts, + ffi.Pointer dist, + int dist_len, + ) { + return _options_set_dist_n( + opts, + dist, + dist_len, + ); + } + + late final _options_set_dist_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Size)>>('sentry_options_set_dist_n'); + late final _options_set_dist_n = _options_set_dist_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Gets the dist. + ffi.Pointer options_get_dist( + ffi.Pointer opts, + ) { + return _options_get_dist( + opts, + ); + } + + late final _options_get_distPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dist'); + late final _options_get_dist = _options_get_distPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Configures the http proxy. + /// + /// The given proxy has to include the full scheme, eg. `http://some.proxy/`. + void options_set_http_proxy( + ffi.Pointer opts, + ffi.Pointer proxy, + ) { + return _options_set_http_proxy( + opts, + proxy, + ); + } + + late final _options_set_http_proxyPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_http_proxy'); + late final _options_set_http_proxy = _options_set_http_proxyPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_http_proxy_n( + ffi.Pointer opts, + ffi.Pointer proxy, + int proxy_len, + ) { + return _options_set_http_proxy_n( + opts, + proxy, + proxy_len, + ); + } + + late final _options_set_http_proxy_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_http_proxy_n'); + late final _options_set_http_proxy_n = + _options_set_http_proxy_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Returns the configured http proxy. + ffi.Pointer options_get_http_proxy( + ffi.Pointer opts, + ) { + return _options_get_http_proxy( + opts, + ); + } + + late final _options_get_http_proxyPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_http_proxy'); + late final _options_get_http_proxy = _options_get_http_proxyPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Configures the path to a file containing ssl certificates for + /// verification. + void options_set_ca_certs( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_ca_certs( + opts, + path, + ); + } + + late final _options_set_ca_certsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_ca_certs'); + late final _options_set_ca_certs = _options_set_ca_certsPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_set_ca_certs_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_set_ca_certs_n( + opts, + path, + path_len, + ); + } + + late final _options_set_ca_certs_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_ca_certs_n'); + late final _options_set_ca_certs_n = _options_set_ca_certs_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Returns the configured path for ca certificates. + ffi.Pointer options_get_ca_certs( + ffi.Pointer opts, + ) { + return _options_get_ca_certs( + opts, + ); + } + + late final _options_get_ca_certsPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_ca_certs'); + late final _options_get_ca_certs = _options_get_ca_certsPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Configures the name of the http transport thread. + void options_set_transport_thread_name( + ffi.Pointer opts, + ffi.Pointer name, + ) { + return _options_set_transport_thread_name( + opts, + name, + ); + } + + late final _options_set_transport_thread_namePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>( + 'sentry_options_set_transport_thread_name'); + late final _options_set_transport_thread_name = + _options_set_transport_thread_namePtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void options_set_transport_thread_name_n( + ffi.Pointer opts, + ffi.Pointer name, + int name_len, + ) { + return _options_set_transport_thread_name_n( + opts, + name, + name_len, + ); + } + + late final _options_set_transport_thread_name_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_transport_thread_name_n'); + late final _options_set_transport_thread_name_n = + _options_set_transport_thread_name_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Returns the configured http transport thread name. + ffi.Pointer options_get_transport_thread_name( + ffi.Pointer opts, + ) { + return _options_get_transport_thread_name( + opts, + ); + } + + late final _options_get_transport_thread_namePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Pointer)>>( + 'sentry_options_get_transport_thread_name'); + late final _options_get_transport_thread_name = + _options_get_transport_thread_namePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Configures the name of the sentry SDK. Returns 0 on success. + int options_set_sdk_name( + ffi.Pointer opts, + ffi.Pointer sdk_name, + ) { + return _options_set_sdk_name( + opts, + sdk_name, + ); + } + + late final _options_set_sdk_namePtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_sdk_name'); + late final _options_set_sdk_name = _options_set_sdk_namePtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer)>(); + + /// Configures the name of the sentry SDK. Returns 0 on success. + int options_set_sdk_name_n( + ffi.Pointer opts, + ffi.Pointer sdk_name, + int sdk_name_len, + ) { + return _options_set_sdk_name_n( + opts, + sdk_name, + sdk_name_len, + ); + } + + late final _options_set_sdk_name_nPtr = _lookup< + ffi.NativeFunction< + ffi.Int Function(ffi.Pointer, ffi.Pointer, + ffi.Size)>>('sentry_options_set_sdk_name_n'); + late final _options_set_sdk_name_n = _options_set_sdk_name_nPtr.asFunction< + int Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Returns the configured sentry SDK name. Unless overwritten this defaults to + /// SENTRY_SDK_NAME. + ffi.Pointer options_get_sdk_name( + ffi.Pointer opts, + ) { + return _options_get_sdk_name( + opts, + ); + } + + late final _options_get_sdk_namePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_sdk_name'); + late final _options_get_sdk_name = _options_get_sdk_namePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Returns the user agent. Unless overwritten this defaults to + /// "SENTRY_SDK_NAME / SENTRY_SDK_VERSION". + ffi.Pointer options_get_user_agent( + ffi.Pointer opts, + ) { + return _options_get_user_agent( + opts, + ); + } + + late final _options_get_user_agentPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_user_agent'); + late final _options_get_user_agent = _options_get_user_agentPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); + + /// Enables or disables debug printing mode. + void options_set_debug( + ffi.Pointer opts, + int debug, + ) { + return _options_set_debug( + opts, + debug, + ); + } + + late final _options_set_debugPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_debug'); + late final _options_set_debug = _options_set_debugPtr + .asFunction, int)>(); + + /// Returns the current value of the debug flag. + int options_get_debug( + ffi.Pointer opts, + ) { + return _options_get_debug( + opts, + ); + } + + late final _options_get_debugPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_debug'); + late final _options_get_debug = _options_get_debugPtr + .asFunction)>(); + + /// Sets the number of breadcrumbs being tracked and attached to events. + /// + /// Defaults to 100. + void options_set_max_breadcrumbs( + ffi.Pointer opts, + int max_breadcrumbs, + ) { + return _options_set_max_breadcrumbs( + opts, + max_breadcrumbs, + ); + } + + late final _options_set_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Size)>>('sentry_options_set_max_breadcrumbs'); + late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr + .asFunction, int)>(); + + /// Gets the number of breadcrumbs being tracked and attached to events. + int options_get_max_breadcrumbs( + ffi.Pointer opts, + ) { + return _options_get_max_breadcrumbs( + opts, + ); + } + + late final _options_get_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_max_breadcrumbs'); + late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr + .asFunction)>(); + + /// 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. + void options_set_logger( + ffi.Pointer opts, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Int32, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>> + func, + ffi.Pointer userdata, + ) { + return _options_set_logger( + opts, + func, + userdata, + ); + } + + late final _options_set_loggerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Int32, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>>('sentry_options_set_logger'); + late final _options_set_logger = _options_set_loggerPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Int32, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>(); + + /// Enables or disables automatic session tracking. + /// + /// Automatic session tracking is enabled by default and is equivalent to calling + /// `sentry_start_session` after startup. + /// There can only be one running session, and the current session will always be + /// closed implicitly by `sentry_close`, when starting a new session with + /// `sentry_start_session`, or manually by calling `sentry_end_session`. + void options_set_auto_session_tracking( + ffi.Pointer opts, + int val, + ) { + return _options_set_auto_session_tracking( + opts, + val, + ); + } + + late final _options_set_auto_session_trackingPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_auto_session_tracking'); + late final _options_set_auto_session_tracking = + _options_set_auto_session_trackingPtr + .asFunction, int)>(); + + /// Returns true if automatic session tracking is enabled. + int options_get_auto_session_tracking( + ffi.Pointer opts, + ) { + return _options_get_auto_session_tracking( + opts, + ); + } + + late final _options_get_auto_session_trackingPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_auto_session_tracking'); + late final _options_get_auto_session_tracking = + _options_get_auto_session_trackingPtr + .asFunction)>(); + + /// Enables or disables user consent requirements for uploads. + /// + /// This disables uploads until the user has given the consent to the SDK. + /// Consent itself is given with `sentry_user_consent_give` and + /// `sentry_user_consent_revoke`. + void options_set_require_user_consent( + ffi.Pointer opts, + int val, + ) { + return _options_set_require_user_consent( + opts, + val, + ); + } + + late final _options_set_require_user_consentPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_require_user_consent'); + late final _options_set_require_user_consent = + _options_set_require_user_consentPtr + .asFunction, int)>(); + + /// Returns true if user consent is required. + int options_get_require_user_consent( + ffi.Pointer opts, + ) { + return _options_get_require_user_consent( + opts, + ); + } + + late final _options_get_require_user_consentPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_require_user_consent'); + late final _options_get_require_user_consent = + _options_get_require_user_consentPtr + .asFunction)>(); + + /// Enables or disables on-device symbolication of stack traces. + /// + /// This feature can have a performance impact, and is enabled by default on + /// Android. It is usually only needed when it is not possible to provide debug + /// information files for system libraries which are needed for serverside + /// symbolication. + void options_set_symbolize_stacktraces( + ffi.Pointer opts, + int val, + ) { + return _options_set_symbolize_stacktraces( + opts, + val, + ); + } + + late final _options_set_symbolize_stacktracesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_symbolize_stacktraces'); + late final _options_set_symbolize_stacktraces = + _options_set_symbolize_stacktracesPtr + .asFunction, int)>(); + + /// Returns true if on-device symbolication of stack traces is enabled. + int options_get_symbolize_stacktraces( + ffi.Pointer opts, + ) { + return _options_get_symbolize_stacktraces( + opts, + ); + } + + late final _options_get_symbolize_stacktracesPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_symbolize_stacktraces'); + late final _options_get_symbolize_stacktraces = + _options_get_symbolize_stacktracesPtr + .asFunction)>(); + + /// Adds a new attachment to be sent along. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// API Users on windows are encouraged to use `sentry_options_add_attachmentw` + /// instead. + void options_add_attachment( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_add_attachment( + opts, + path, + ); + } + + late final _options_add_attachmentPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_add_attachment'); + late final _options_add_attachment = _options_add_attachmentPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_add_attachment_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_add_attachment_n( + opts, + path, + path_len, + ); + } + + late final _options_add_attachment_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_add_attachment_n'); + late final _options_add_attachment_n = + _options_add_attachment_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets the path to the crashpad handler if the crashpad backend is used. + /// + /// The path defaults to the `crashpad_handler`/`crashpad_handler.exe` + /// executable, depending on platform, which is expected to be present in the + /// same directory as the app executable. + /// + /// It is recommended that library users set an explicit handler path, depending + /// on the directory/executable structure of their app. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// API Users on windows are encouraged to use `sentry_options_set_handler_pathw` + /// instead. + void options_set_handler_path( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_handler_path( + opts, + path, + ); + } + + late final _options_set_handler_pathPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_handler_path'); + late final _options_set_handler_path = + _options_set_handler_pathPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void options_set_handler_path_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_set_handler_path_n( + opts, + path, + path_len, + ); + } + + late final _options_set_handler_path_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_handler_path_n'); + late final _options_set_handler_path_n = + _options_set_handler_path_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets the path to the Sentry Database Directory. + /// + /// Sentry will use this path to persist user consent, sessions, and other + /// artifacts in case of a crash. This will also be used by the crashpad backend + /// if it is configured. + /// + /// The directory is used for "cached" data, which needs to persist across + /// application restarts to ensure proper flagging of release-health sessions, + /// but might otherwise be safely purged regularly. + /// + /// It is roughly equivalent to the type of `AppData/Local` on Windows and + /// `XDG_CACHE_HOME` on Linux, and equivalent runtime directories on other + /// platforms. + /// + /// It is recommended that users set an explicit absolute path, depending + /// on their apps runtime directory. The path will be created if it does not + /// exist, and will be resolved to an absolute path inside of `sentry_init`. The + /// directory should not be shared with other application data/configuration, as + /// sentry-native will enumerate and possibly delete files in that directory. An + /// example might be `$XDG_CACHE_HOME/your-app/sentry` + /// + /// If no explicit path it set, sentry-native will default to `.sentry-native` in + /// the current working directory, with no specific platform-specific handling. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// API Users on windows are encouraged to use + /// `sentry_options_set_database_pathw` instead. + void options_set_database_path( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_database_path( + opts, + path, + ); + } + + late final _options_set_database_pathPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_database_path'); + late final _options_set_database_path = + _options_set_database_pathPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void options_set_database_path_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_set_database_path_n( + opts, + path, + path_len, + ); + } + + late final _options_set_database_path_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_database_path_n'); + late final _options_set_database_path_n = + _options_set_database_path_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Wide char version of `sentry_options_add_attachment`. + void options_add_attachmentw( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_add_attachmentw( + opts, + path, + ); + } + + late final _options_add_attachmentwPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_add_attachmentw'); + late final _options_add_attachmentw = _options_add_attachmentwPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void options_add_attachmentw_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_add_attachmentw_n( + opts, + path, + path_len, + ); + } + + late final _options_add_attachmentw_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_add_attachmentw_n'); + late final _options_add_attachmentw_n = + _options_add_attachmentw_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Wide char version of `sentry_options_set_handler_path`. + void options_set_handler_pathw( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_handler_pathw( + opts, + path, + ); + } + + late final _options_set_handler_pathwPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_handler_pathw'); + late final _options_set_handler_pathw = + _options_set_handler_pathwPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void options_set_handler_pathw_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_set_handler_pathw_n( + opts, + path, + path_len, + ); + } + + late final _options_set_handler_pathw_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_handler_pathw_n'); + late final _options_set_handler_pathw_n = + _options_set_handler_pathw_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Wide char version of `sentry_options_set_database_path`. + void options_set_database_pathw( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_database_pathw( + opts, + path, + ); + } + + late final _options_set_database_pathwPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_database_pathw'); + late final _options_set_database_pathw = + _options_set_database_pathwPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void options_set_database_pathw_n( + ffi.Pointer opts, + ffi.Pointer path, + int path_len, + ) { + return _options_set_database_pathw_n( + opts, + path, + path_len, + ); + } + + late final _options_set_database_pathw_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_options_set_database_pathw_n'); + late final _options_set_database_pathw_n = + _options_set_database_pathw_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Enables forwarding to the system crash reporter. Disabled by default. + /// + /// This setting only has an effect when using Crashpad on macOS. If enabled, + /// Crashpad forwards crashes to the macOS system crash reporter. Depending + /// on the crash, this may impact the crash time. Even if enabled, Crashpad + /// may choose not to forward certain crashes. + void options_set_system_crash_reporter_enabled( + ffi.Pointer opts, + int enabled, + ) { + return _options_set_system_crash_reporter_enabled( + opts, + enabled, + ); + } + + late final _options_set_system_crash_reporter_enabledPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_system_crash_reporter_enabled'); + late final _options_set_system_crash_reporter_enabled = + _options_set_system_crash_reporter_enabledPtr + .asFunction, int)>(); + + /// Sets the maximum time (in milliseconds) to wait for the asynchronous tasks to + /// end on shutdown, before attempting a forced termination. + void options_set_shutdown_timeout( + ffi.Pointer opts, + int shutdown_timeout, + ) { + return _options_set_shutdown_timeout( + opts, + shutdown_timeout, + ); + } + + late final _options_set_shutdown_timeoutPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Uint64)>>('sentry_options_set_shutdown_timeout'); + late final _options_set_shutdown_timeout = _options_set_shutdown_timeoutPtr + .asFunction, int)>(); + + /// Gets the maximum time (in milliseconds) to wait for the asynchronous tasks to + /// end on shutdown, before attempting a forced termination. + int options_get_shutdown_timeout( + ffi.Pointer opts, + ) { + return _options_get_shutdown_timeout( + opts, + ); + } + + late final _options_get_shutdown_timeoutPtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_options_get_shutdown_timeout'); + late final _options_get_shutdown_timeout = _options_get_shutdown_timeoutPtr + .asFunction)>(); + + /// Sets a user-defined backend. + /// + /// Since creation and destruction of backends is not exposed in the API, this + /// can only be used to set the backend to `NULL`, which disables the backend in + /// the initialization. + void options_set_backend( + ffi.Pointer opts, + ffi.Pointer backend, + ) { + return _options_set_backend( + opts, + backend, + ); + } + + late final _options_set_backendPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_backend'); + late final _options_set_backend = _options_set_backendPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + /// Initializes the Sentry SDK with the specified options. + /// + /// This takes ownership of the options. After the options have been set + /// they cannot be modified any more. + /// Depending on the configured transport and backend, this function might not be + /// fully thread-safe. + /// Returns 0 on success. + int init( + ffi.Pointer options, + ) { + return _init( + options, + ); + } + + late final _initPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_init'); + late final _init = + _initPtr.asFunction)>(); + + /// Instructs the transport to flush its send queue. + /// + /// The `timeout` parameter is in milliseconds. + /// + /// Returns 0 on success, or a non-zero return value in case the timeout is hit. + /// + /// Note that this function will block the thread it was called from until the + /// sentry background worker has finished its work or it timed out, whichever + /// comes first. + int flush( + int timeout, + ) { + return _flush( + timeout, + ); + } + + late final _flushPtr = + _lookup>('sentry_flush'); + late final _flush = _flushPtr.asFunction(); + + /// Shuts down the sentry client and forces transports to flush out. + /// + /// Returns 0 on success. + /// + /// Note that this does not uninstall any crash handler installed by our + /// backends, which will still process crashes after `sentry_close()`, except + /// when using `crashpad` on Linux or the `inproc` backend. + /// + /// Further note that this function will block the thread it was called from + /// until the sentry background worker has finished its work or it timed out, + /// whichever comes first. + int close() { + return _close(); + } + + late final _closePtr = + _lookup>('sentry_close'); + late final _close = _closePtr.asFunction(); + + /// Shuts down the sentry client and forces transports to flush out. + /// + /// This is a **deprecated** alias for `sentry_close`. + /// + /// Returns 0 on success. + int shutdown() { + return _shutdown(); + } + + late final _shutdownPtr = + _lookup>('sentry_shutdown'); + late final _shutdown = _shutdownPtr.asFunction(); + + /// This will lazily load and cache a list of all the loaded libraries. + /// + /// Returns a new reference to an immutable, frozen list. + /// The reference must be released with `sentry_value_decref`. + sentry_value_u get_modules_list() { + return _get_modules_list(); + } + + late final _get_modules_listPtr = + _lookup>( + 'sentry_get_modules_list'); + late final _get_modules_list = + _get_modules_listPtr.asFunction(); + + /// Clears the internal module cache. + /// + /// For performance reasons, sentry will cache the list of loaded libraries when + /// capturing events. This cache can get out-of-date when loading or unloading + /// libraries at runtime. It is therefore recommended to call + /// `sentry_clear_modulecache` when doing so, to make sure that the next call to + /// `sentry_capture_event` will have an up-to-date module list. + void clear_modulecache() { + return _clear_modulecache(); + } + + late final _clear_modulecachePtr = + _lookup>( + 'sentry_clear_modulecache'); + late final _clear_modulecache = + _clear_modulecachePtr.asFunction(); + + /// Re-initializes the Sentry backend. + /// + /// This is needed if a third-party library overrides the previously installed + /// signal handler. Calling this function can be potentially dangerous and should + /// only be done when necessary. + /// + /// Returns 0 on success. + int reinstall_backend() { + return _reinstall_backend(); + } + + late final _reinstall_backendPtr = + _lookup>( + 'sentry_reinstall_backend'); + late final _reinstall_backend = + _reinstall_backendPtr.asFunction(); + + /// Gives user consent. + void user_consent_give() { + return _user_consent_give(); + } + + late final _user_consent_givePtr = + _lookup>( + 'sentry_user_consent_give'); + late final _user_consent_give = + _user_consent_givePtr.asFunction(); + + /// Revokes user consent. + void user_consent_revoke() { + return _user_consent_revoke(); + } + + late final _user_consent_revokePtr = + _lookup>( + 'sentry_user_consent_revoke'); + late final _user_consent_revoke = + _user_consent_revokePtr.asFunction(); + + /// Resets the user consent (back to unknown). + void user_consent_reset() { + return _user_consent_reset(); + } + + late final _user_consent_resetPtr = + _lookup>( + 'sentry_user_consent_reset'); + late final _user_consent_reset = + _user_consent_resetPtr.asFunction(); + + /// Checks the current state of user consent. + int user_consent_get() { + return _user_consent_get(); + } + + late final _user_consent_getPtr = + _lookup>( + 'sentry_user_consent_get'); + late final _user_consent_get = + _user_consent_getPtr.asFunction(); + + /// Sends a sentry event. + /// + /// If returns a nil UUID if the event being passed in is a transaction, and the + /// transaction will not be sent nor consumed. `sentry_transaction_finish` should + /// be used to send transactions. + sentry_uuid_s capture_event( + sentry_value_u event, + ) { + return _capture_event( + event, + ); + } + + late final _capture_eventPtr = + _lookup>( + 'sentry_capture_event'); + late final _capture_event = + _capture_eventPtr.asFunction(); + + /// Captures an exception to be handled by the backend. + /// + /// This is safe to be called from a crashing thread and may not return. + void handle_exception( + ffi.Pointer uctx, + ) { + return _handle_exception( + uctx, + ); + } + + late final _handle_exceptionPtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_handle_exception'); + late final _handle_exception = _handle_exceptionPtr + .asFunction)>(); + + /// Adds the breadcrumb to be sent in case of an event. + void add_breadcrumb( + sentry_value_u breadcrumb, + ) { + return _add_breadcrumb( + breadcrumb, + ); + } + + late final _add_breadcrumbPtr = + _lookup>( + 'sentry_add_breadcrumb'); + late final _add_breadcrumb = + _add_breadcrumbPtr.asFunction(); + + /// Sets the specified user. + void set_user( + sentry_value_u user, + ) { + return _set_user( + user, + ); + } + + late final _set_userPtr = + _lookup>( + 'sentry_set_user'); + late final _set_user = + _set_userPtr.asFunction(); + + /// Removes a user. + void remove_user() { + return _remove_user(); + } + + late final _remove_userPtr = + _lookup>('sentry_remove_user'); + late final _remove_user = _remove_userPtr.asFunction(); + + /// Sets a tag. + void set_tag( + ffi.Pointer key, + ffi.Pointer value, + ) { + return _set_tag( + key, + value, + ); + } + + late final _set_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>('sentry_set_tag'); + late final _set_tag = _set_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void set_tag_n( + ffi.Pointer key, + int key_len, + ffi.Pointer value, + int value_len, + ) { + return _set_tag_n( + key, + key_len, + value, + value_len, + ); + } + + late final _set_tag_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Size, + ffi.Pointer, ffi.Size)>>('sentry_set_tag_n'); + late final _set_tag_n = _set_tag_nPtr.asFunction< + void Function(ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Removes the tag with the specified key. + void remove_tag( + ffi.Pointer key, + ) { + return _remove_tag( + key, + ); + } + + late final _remove_tagPtr = + _lookup)>>( + 'sentry_remove_tag'); + late final _remove_tag = + _remove_tagPtr.asFunction)>(); + + void remove_tag_n( + ffi.Pointer key, + int key_len, + ) { + return _remove_tag_n( + key, + key_len, + ); + } + + late final _remove_tag_nPtr = _lookup< + ffi + .NativeFunction, ffi.Size)>>( + 'sentry_remove_tag_n'); + late final _remove_tag_n = + _remove_tag_nPtr.asFunction, int)>(); + + /// Sets extra information. + void set_extra( + ffi.Pointer key, + sentry_value_u value, + ) { + return _set_extra( + key, + value, + ); + } + + late final _set_extraPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, sentry_value_u)>>('sentry_set_extra'); + late final _set_extra = _set_extraPtr + .asFunction, sentry_value_u)>(); + + void set_extra_n( + ffi.Pointer key, + int key_len, + sentry_value_u value, + ) { + return _set_extra_n( + key, + key_len, + value, + ); + } + + late final _set_extra_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Size, + sentry_value_u)>>('sentry_set_extra_n'); + late final _set_extra_n = _set_extra_nPtr + .asFunction, int, sentry_value_u)>(); + + /// Removes the extra with the specified key. + void remove_extra( + ffi.Pointer key, + ) { + return _remove_extra( + key, + ); + } + + late final _remove_extraPtr = + _lookup)>>( + 'sentry_remove_extra'); + late final _remove_extra = + _remove_extraPtr.asFunction)>(); + + void remove_extra_n( + ffi.Pointer key, + int key_len, + ) { + return _remove_extra_n( + key, + key_len, + ); + } + + late final _remove_extra_nPtr = _lookup< + ffi + .NativeFunction, ffi.Size)>>( + 'sentry_remove_extra_n'); + late final _remove_extra_n = _remove_extra_nPtr + .asFunction, int)>(); + + /// Sets a context object. + void set_context( + ffi.Pointer key, + sentry_value_u value, + ) { + return _set_context( + key, + value, + ); + } + + late final _set_contextPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, sentry_value_u)>>('sentry_set_context'); + late final _set_context = _set_contextPtr + .asFunction, sentry_value_u)>(); + + void set_context_n( + ffi.Pointer key, + int key_len, + sentry_value_u value, + ) { + return _set_context_n( + key, + key_len, + value, + ); + } + + late final _set_context_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Size, + sentry_value_u)>>('sentry_set_context_n'); + late final _set_context_n = _set_context_nPtr + .asFunction, int, sentry_value_u)>(); + + /// Removes the context object with the specified key. + void remove_context( + ffi.Pointer key, + ) { + return _remove_context( + key, + ); + } + + late final _remove_contextPtr = + _lookup)>>( + 'sentry_remove_context'); + late final _remove_context = + _remove_contextPtr.asFunction)>(); + + void remove_context_n( + ffi.Pointer key, + int key_len, + ) { + return _remove_context_n( + key, + key_len, + ); + } + + late final _remove_context_nPtr = _lookup< + ffi + .NativeFunction, ffi.Size)>>( + 'sentry_remove_context_n'); + late final _remove_context_n = _remove_context_nPtr + .asFunction, int)>(); + + /// Sets the event fingerprint. + /// + /// This accepts a variable number of arguments, and needs to be terminated by a + /// trailing `NULL`. + void set_fingerprint( + ffi.Pointer fingerprint, + ) { + return _set_fingerprint( + fingerprint, + ); + } + + late final _set_fingerprintPtr = + _lookup)>>( + 'sentry_set_fingerprint'); + late final _set_fingerprint = + _set_fingerprintPtr.asFunction)>(); + + void set_fingerprint_n( + ffi.Pointer fingerprint, + int fingerprint_len, + ) { + return _set_fingerprint_n( + fingerprint, + fingerprint_len, + ); + } + + late final _set_fingerprint_nPtr = _lookup< + ffi + .NativeFunction, ffi.Size)>>( + 'sentry_set_fingerprint_n'); + late final _set_fingerprint_n = _set_fingerprint_nPtr + .asFunction, int)>(); + + /// Removes the fingerprint. + void remove_fingerprint() { + return _remove_fingerprint(); + } + + late final _remove_fingerprintPtr = + _lookup>( + 'sentry_remove_fingerprint'); + late final _remove_fingerprint = + _remove_fingerprintPtr.asFunction(); + + /// Sets the transaction. + void set_transaction( + ffi.Pointer transaction, + ) { + return _set_transaction( + transaction, + ); + } + + late final _set_transactionPtr = + _lookup)>>( + 'sentry_set_transaction'); + late final _set_transaction = + _set_transactionPtr.asFunction)>(); + + void set_transaction_n( + ffi.Pointer transaction, + int transaction_len, + ) { + return _set_transaction_n( + transaction, + transaction_len, + ); + } + + late final _set_transaction_nPtr = _lookup< + ffi + .NativeFunction, ffi.Size)>>( + 'sentry_set_transaction_n'); + late final _set_transaction_n = _set_transaction_nPtr + .asFunction, int)>(); + + /// Sets the event level. + void set_level( + int level, + ) { + return _set_level( + level, + ); + } + + late final _set_levelPtr = + _lookup>( + 'sentry_set_level'); + late final _set_level = _set_levelPtr.asFunction(); + + /// Sets the maximum number of spans that can be attached to a + /// transaction. + void options_set_max_spans( + ffi.Pointer opts, + int max_spans, + ) { + return _options_set_max_spans( + opts, + max_spans, + ); + } + + late final _options_set_max_spansPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Size)>>('sentry_options_set_max_spans'); + late final _options_set_max_spans = _options_set_max_spansPtr + .asFunction, int)>(); + + /// Gets the maximum number of spans that can be attached to a + /// transaction. + int options_get_max_spans( + ffi.Pointer opts, + ) { + return _options_get_max_spans( + opts, + ); + } + + late final _options_get_max_spansPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_max_spans'); + late final _options_get_max_spans = _options_get_max_spansPtr + .asFunction)>(); + + /// Sets the sample rate for transactions. Should be a double between + /// `0.0` and `1.0`. Transactions will be randomly discarded during + /// `sentry_transaction_finish` when the sample rate is < 1.0. + void options_set_traces_sample_rate( + ffi.Pointer opts, + double sample_rate, + ) { + return _options_set_traces_sample_rate( + opts, + sample_rate, + ); + } + + late final _options_set_traces_sample_ratePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Double)>>('sentry_options_set_traces_sample_rate'); + late final _options_set_traces_sample_rate = + _options_set_traces_sample_ratePtr + .asFunction, double)>(); + + /// Returns the sample rate for transactions. + double options_get_traces_sample_rate( + ffi.Pointer opts, + ) { + return _options_get_traces_sample_rate( + opts, + ); + } + + late final _options_get_traces_sample_ratePtr = _lookup< + ffi + .NativeFunction)>>( + 'sentry_options_get_traces_sample_rate'); + late final _options_get_traces_sample_rate = + _options_get_traces_sample_ratePtr + .asFunction)>(); + + /// Starts a new session. + void start_session() { + return _start_session(); + } + + late final _start_sessionPtr = + _lookup>('sentry_start_session'); + late final _start_session = _start_sessionPtr.asFunction(); + + /// Ends a session. + void end_session() { + return _end_session(); + } + + late final _end_sessionPtr = + _lookup>('sentry_end_session'); + late final _end_session = _end_sessionPtr.asFunction(); + + /// Ends a session with an explicit `status` code. + void end_session_with_status( + int status, + ) { + return _end_session_with_status( + status, + ); + } + + late final _end_session_with_statusPtr = + _lookup>( + 'sentry_end_session_with_status'); + late final _end_session_with_status = + _end_session_with_statusPtr.asFunction(); + + /// Constructs a new Transaction Context. The returned value needs to be passed + /// into `sentry_transaction_start` in order to be recorded and sent to sentry. + /// + /// See + /// https://docs.sentry.io/platforms/native/enriching-events/transaction-name/ + /// for an explanation of a Transaction's `name`, and + /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions + /// around an `operation`'s value. + /// + /// Also see https://develop.sentry.dev/sdk/event-payloads/transaction/#anatomy + /// for an explanation of `operation`, in addition to other properties and + /// actions that can be performed on a Transaction. + /// + /// The returned value is not thread-safe. Users are expected to ensure that + /// appropriate locking mechanisms are implemented over the Transaction Context + /// if it needs to be mutated across threads. Methods operating on the + /// Transaction Context will mention what kind of expectations they carry if they + /// need to mutate or access the object in a thread-safe way. + ffi.Pointer transaction_context_new( + ffi.Pointer name, + ffi.Pointer operation, + ) { + return _transaction_context_new( + name, + operation, + ); + } + + late final _transaction_context_newPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_context_new'); + late final _transaction_context_new = _transaction_context_newPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer transaction_context_new_n( + ffi.Pointer name, + int name_len, + ffi.Pointer operation, + int operation_len, + ) { + return _transaction_context_new_n( + name, + name_len, + operation, + operation_len, + ); + } + + late final _transaction_context_new_nPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_context_new_n'); + late final _transaction_context_new_n = + _transaction_context_new_nPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Sets the `name` on a Transaction Context, which will be used in the + /// Transaction constructed off of the context. + /// + /// The Transaction Context should not be mutated by other functions while + /// setting a name on it. + void transaction_context_set_name( + ffi.Pointer tx_cxt, + ffi.Pointer name, + ) { + return _transaction_context_set_name( + tx_cxt, + name, + ); + } + + late final _transaction_context_set_namePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_context_set_name'); + late final _transaction_context_set_name = + _transaction_context_set_namePtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer)>(); + + void transaction_context_set_name_n( + ffi.Pointer tx_cxt, + ffi.Pointer name, + int name_len, + ) { + return _transaction_context_set_name_n( + tx_cxt, + name, + name_len, + ); + } + + late final _transaction_context_set_name_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_context_set_name_n'); + late final _transaction_context_set_name_n = + _transaction_context_set_name_nPtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer, int)>(); + + /// Sets the `operation` on a Transaction Context, which will be used in the + /// Transaction constructed off of the context + /// + /// See https://develop.sentry.dev/sdk/performance/span-operations/ for + /// conventions on `operation`s. + /// + /// The Transaction Context should not be mutated by other functions while + /// setting an operation on it. + void transaction_context_set_operation( + ffi.Pointer tx_cxt, + ffi.Pointer operation, + ) { + return _transaction_context_set_operation( + tx_cxt, + operation, + ); + } + + late final _transaction_context_set_operationPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>( + 'sentry_transaction_context_set_operation'); + late final _transaction_context_set_operation = + _transaction_context_set_operationPtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer)>(); + + void transaction_context_set_operation_n( + ffi.Pointer tx_cxt, + ffi.Pointer operation, + int operation_len, + ) { + return _transaction_context_set_operation_n( + tx_cxt, + operation, + operation_len, + ); + } + + late final _transaction_context_set_operation_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_context_set_operation_n'); + late final _transaction_context_set_operation_n = + _transaction_context_set_operation_nPtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer, int)>(); + + /// Sets the `sampled` field on a Transaction Context, which will be used in the + /// Transaction constructed off of the context. + /// + /// When passed any value above 0, the Transaction will bypass all sampling + /// options and always be sent to sentry. If passed 0, this Transaction and its + /// child spans will never be sent to sentry. + /// + /// The Transaction Context should not be mutated by other functions while + /// setting `sampled` on it. + void transaction_context_set_sampled( + ffi.Pointer tx_cxt, + int sampled, + ) { + return _transaction_context_set_sampled( + tx_cxt, + sampled, + ); + } + + late final _transaction_context_set_sampledPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_transaction_context_set_sampled'); + late final _transaction_context_set_sampled = + _transaction_context_set_sampledPtr.asFunction< + void Function(ffi.Pointer, int)>(); + + /// Removes the `sampled` field on a Transaction Context, which will be used in + /// the Transaction constructed off of the context. + /// + /// The Transaction will use the sampling rate as defined in `sentry_options`. + /// + /// The Transaction Context should not be mutated by other functions while + /// removing `sampled`. + void transaction_context_remove_sampled( + ffi.Pointer tx_cxt, + ) { + return _transaction_context_remove_sampled( + tx_cxt, + ); + } + + late final _transaction_context_remove_sampledPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>>( + 'sentry_transaction_context_remove_sampled'); + late final _transaction_context_remove_sampled = + _transaction_context_remove_sampledPtr.asFunction< + void Function(ffi.Pointer)>(); + + /// Update the Transaction Context with the given HTTP header key/value pair. + /// + /// This is used to propagate distributed tracing metadata from upstream + /// services. Therefore, the headers of incoming requests should be fed into this + /// function so that sentry is able to continue a trace that was started by an + /// upstream service. + void transaction_context_update_from_header( + ffi.Pointer tx_cxt, + ffi.Pointer key, + ffi.Pointer value, + ) { + return _transaction_context_update_from_header( + tx_cxt, + key, + value, + ); + } + + late final _transaction_context_update_from_headerPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>( + 'sentry_transaction_context_update_from_header'); + late final _transaction_context_update_from_header = + _transaction_context_update_from_headerPtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + void transaction_context_update_from_header_n( + ffi.Pointer tx_cxt, + ffi.Pointer key, + int key_len, + ffi.Pointer value, + int value_len, + ) { + return _transaction_context_update_from_header_n( + tx_cxt, + key, + key_len, + value, + value_len, + ); + } + + late final _transaction_context_update_from_header_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_context_update_from_header_n'); + late final _transaction_context_update_from_header_n = + _transaction_context_update_from_header_nPtr.asFunction< + void Function(ffi.Pointer, + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Starts a new Transaction based on the provided context, restored from an + /// external integration (i.e. a span from a different SDK) or manually + /// constructed by a user. + /// + /// The second parameter is a custom Sampling Context to be used with a Traces + /// Sampler to make a more informed sampling decision. The SDK does not currently + /// support a custom Traces Sampler and this parameter is ignored for the time + /// being but needs to be provided. + /// + /// Returns a Transaction, which is expected to be manually managed by the + /// caller. Manual management involves ensuring that `sentry_transaction_finish` + /// is invoked for the Transaction, and that the caller manually starts and + /// finishes any child Spans as needed on the Transaction. + /// + /// Not invoking `sentry_transaction_finish` with the returned Transaction means + /// it will be discarded, and will not be sent to sentry. + /// + /// To ensure that any Events or Message Events are associated with this + /// Transaction while it is active, invoke and pass in the Transaction returned + /// by this function to `sentry_set_transaction_object`. Further documentation on + /// this can be found in `sentry_set_transaction_object`'s docstring. + /// + /// Takes ownership of `transaction_context`. A Transaction Context cannot be + /// modified or re-used after it is used to start a Transaction. + /// + /// The returned value is not thread-safe. Users are expected to ensure that + /// appropriate locking mechanisms are implemented over the Transaction if it + /// needs to be mutated across threads. Methods operating on the Transaction will + /// mention what kind of expectations they carry if they need to mutate or access + /// the object in a thread-safe way. + ffi.Pointer transaction_start( + ffi.Pointer tx_cxt, + sentry_value_u sampling_ctx, + ) { + return _transaction_start( + tx_cxt, + sampling_ctx, + ); + } + + late final _transaction_startPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + sentry_value_u)>>('sentry_transaction_start'); + late final _transaction_start = _transaction_startPtr.asFunction< + ffi.Pointer Function( + ffi.Pointer, sentry_value_u)>(); + + /// Finishes and sends a Transaction to sentry. The event ID of the Transaction + /// will be returned if this was successful; A nil UUID will be returned + /// otherwise. + /// + /// Always takes ownership of `transaction`, regardless of whether the operation + /// was successful or not. A Transaction cannot be modified or re-used after it + /// is finished. + sentry_uuid_s transaction_finish( + ffi.Pointer tx, + ) { + return _transaction_finish( + tx, + ); + } + + late final _transaction_finishPtr = _lookup< + ffi.NativeFunction< + sentry_uuid_s Function( + ffi.Pointer)>>('sentry_transaction_finish'); + late final _transaction_finish = _transaction_finishPtr + .asFunction)>(); + + /// Sets the Transaction so any Events sent while the Transaction + /// is active will be associated with the Transaction. + /// + /// If the Transaction being passed in is unsampled, it will still be associated + /// with any new Events. This will lead to some Events pointing to orphan or + /// missing traces in sentry, see + /// https://docs.sentry.io/product/sentry-basics/tracing/trace-view/#orphan-traces-and-broken-subtraces + /// + /// This increases the number of references pointing to the Transaction. Invoke + /// `sentry_transaction_finish` to remove the Transaction set by this function as + /// well as its reference by passing in the same Transaction as the one passed + /// into this function. + void set_transaction_object( + ffi.Pointer tx, + ) { + return _set_transaction_object( + tx, + ); + } + + late final _set_transaction_objectPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer)>>( + 'sentry_set_transaction_object'); + late final _set_transaction_object = _set_transaction_objectPtr + .asFunction)>(); + + /// Sets the Span so any Events sent while the Span + /// is active will be associated with the Span. + /// + /// This increases the number of references pointing to the Span. Invoke + /// `sentry_span_finish` to remove the Span set by this function as well + /// as its reference by passing in the same Span as the one passed into + /// this function. + void set_span( + ffi.Pointer span, + ) { + return _set_span( + span, + ); + } + + late final _set_spanPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_set_span'); + late final _set_span = + _set_spanPtr.asFunction)>(); + + /// Starts a new Span. + /// + /// The return value of `sentry_transaction_start` should be passed in as + /// `parent`. + /// + /// Both `operation` and `description` can be null, but it is recommended to + /// supply the former. See + /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions + /// around operations. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/span/ for a description of + /// the created Span's properties and expectations for `operation` and + /// `description`. + /// + /// Returns a value that should be passed into `sentry_span_finish`. Not + /// finishing the Span means it will be discarded, and will not be sent to + /// sentry. `sentry_value_null` will be returned if the child Span could not be + /// created. + /// + /// To ensure that any Events or Message Events are associated with this + /// Span while it is active, invoke and pass in the Span returned + /// by this function to `sentry_set_span`. Further documentation on this can be + /// found in `sentry_set_span`'s docstring. + /// + /// This increases the number of references pointing to the Transaction. + /// + /// The returned value is not thread-safe. Users are expected to ensure that + /// appropriate locking mechanisms are implemented over the Span if it needs + /// to be mutated across threads. Methods operating on the Span will mention what + /// kind of expectations they carry if they need to mutate or access the object + /// in a thread-safe way. + ffi.Pointer transaction_start_child( + ffi.Pointer parent, + ffi.Pointer operation, + ffi.Pointer description, + ) { + return _transaction_start_child( + parent, + operation, + description, + ); + } + + late final _transaction_start_childPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_start_child'); + late final _transaction_start_child = _transaction_start_childPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer transaction_start_child_n( + ffi.Pointer parent, + ffi.Pointer operation, + int operation_len, + ffi.Pointer description, + int description_len, + ) { + return _transaction_start_child_n( + parent, + operation, + operation_len, + description, + description_len, + ); + } + + late final _transaction_start_child_nPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_start_child_n'); + late final _transaction_start_child_n = + _transaction_start_child_nPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Starts a new Span. + /// + /// The return value of `sentry_span_start_child` may be passed in as `parent`. + /// + /// Both `operation` and `description` can be null, but it is recommended to + /// supply the former. See + /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions + /// around operations. + /// + /// See https://develop.sentry.dev/sdk/event-payloads/span/ for a description of + /// the created Span's properties and expectations for `operation` and + /// `description`. + /// + /// Returns a value that should be passed into `sentry_span_finish`. Not + /// finishing the Span means it will be discarded, and will not be sent to + /// sentry. `sentry_value_null` will be returned if the child Span could not be + /// created. + /// + /// To ensure that any Events or Message Events are associated with this + /// Span while it is active, invoke and pass in the Span returned + /// by this function to `sentry_set_span`. Further documentation on this can be + /// found in `sentry_set_span`'s docstring. + /// + /// The returned value is not thread-safe. Users are expected to ensure that + /// appropriate locking mechanisms are implemented over the Span if it needs + /// to be mutated across threads. Methods operating on the Span will mention what + /// kind of expectations they carry if they need to mutate or access the object + /// in a thread-safe way. + ffi.Pointer span_start_child( + ffi.Pointer parent, + ffi.Pointer operation, + ffi.Pointer description, + ) { + return _span_start_child( + parent, + operation, + description, + ); + } + + late final _span_start_childPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('sentry_span_start_child'); + late final _span_start_child = _span_start_childPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + ffi.Pointer span_start_child_n( + ffi.Pointer parent, + ffi.Pointer operation, + int operation_len, + ffi.Pointer description, + int description_len, + ) { + return _span_start_child_n( + parent, + operation, + operation_len, + description, + description_len, + ); + } + + late final _span_start_child_nPtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_span_start_child_n'); + late final _span_start_child_n = _span_start_child_nPtr.asFunction< + ffi.Pointer Function(ffi.Pointer, + ffi.Pointer, int, ffi.Pointer, int)>(); + + /// Finishes a Span. + /// + /// This takes ownership of `span`. A Span cannot be modified or re-used after it + /// is finished. + /// + /// This will mutate the `span`'s containing Transaction, so the containing + /// Transaction should also not be mutated by other functions when finishing a + /// span. + void span_finish( + ffi.Pointer span, + ) { + return _span_finish( + span, + ); + } + + late final _span_finishPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_span_finish'); + late final _span_finish = + _span_finishPtr.asFunction)>(); + + /// Sets a tag on a Transaction to the given string value. + /// + /// Tags longer than 200 bytes will be truncated. + /// + /// The Transaction should not be mutated by other functions while a tag is being + /// set on it. + void transaction_set_tag( + ffi.Pointer transaction, + ffi.Pointer tag, + ffi.Pointer value, + ) { + return _transaction_set_tag( + transaction, + tag, + value, + ); + } + + late final _transaction_set_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_set_tag'); + late final _transaction_set_tag = _transaction_set_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(); + + void transaction_set_tag_n( + ffi.Pointer transaction, + ffi.Pointer tag, + int tag_len, + ffi.Pointer value, + int value_len, + ) { + return _transaction_set_tag_n( + transaction, + tag, + tag_len, + value, + value_len, + ); + } + + late final _transaction_set_tag_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_set_tag_n'); + late final _transaction_set_tag_n = _transaction_set_tag_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + int, ffi.Pointer, int)>(); + + /// Removes a tag from a Transaction. + /// + /// The Transaction should not be mutated by other functions while a tag is being + /// removed from it. + void transaction_remove_tag( + ffi.Pointer transaction, + ffi.Pointer tag, + ) { + return _transaction_remove_tag( + transaction, + tag, + ); + } + + late final _transaction_remove_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_remove_tag'); + late final _transaction_remove_tag = _transaction_remove_tagPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void transaction_remove_tag_n( + ffi.Pointer transaction, + ffi.Pointer tag, + int tag_len, + ) { + return _transaction_remove_tag_n( + transaction, + tag, + tag_len, + ); + } + + late final _transaction_remove_tag_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_remove_tag_n'); + late final _transaction_remove_tag_n = + _transaction_remove_tag_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets the given key in a Transaction's "data" section to the given value. + /// + /// The Transaction should not be mutated by other functions while data is being + /// set on it. + void transaction_set_data( + ffi.Pointer transaction, + ffi.Pointer key, + sentry_value_u value, + ) { + return _transaction_set_data( + transaction, + key, + value, + ); + } + + late final _transaction_set_dataPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + sentry_value_u)>>('sentry_transaction_set_data'); + late final _transaction_set_data = _transaction_set_dataPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + sentry_value_u)>(); + + void transaction_set_data_n( + ffi.Pointer transaction, + ffi.Pointer key, + int key_len, + sentry_value_u value, + ) { + return _transaction_set_data_n( + transaction, + key, + key_len, + value, + ); + } + + late final _transaction_set_data_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + sentry_value_u)>>('sentry_transaction_set_data_n'); + late final _transaction_set_data_n = _transaction_set_data_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + int, sentry_value_u)>(); + + /// Removes a key from a Transaction's "data" section. + /// + /// The Transaction should not be mutated by other functions while data is being + /// removed from it. + void transaction_remove_data( + ffi.Pointer transaction, + ffi.Pointer key, + ) { + return _transaction_remove_data( + transaction, + key, + ); + } + + late final _transaction_remove_dataPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_remove_data'); + late final _transaction_remove_data = _transaction_remove_dataPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void transaction_remove_data_n( + ffi.Pointer transaction, + ffi.Pointer key, + int key_len, + ) { + return _transaction_remove_data_n( + transaction, + key, + key_len, + ); + } + + late final _transaction_remove_data_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_remove_data_n'); + late final _transaction_remove_data_n = + _transaction_remove_data_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets a tag on a Span to the given string value. + /// + /// Tags longer than 200 bytes will be truncated. + /// + /// The Span should not be mutated by other functions while a tag is being set on + /// it. + void span_set_tag( + ffi.Pointer span, + ffi.Pointer tag, + ffi.Pointer value, + ) { + return _span_set_tag( + span, + tag, + value, + ); + } + + late final _span_set_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>>('sentry_span_set_tag'); + late final _span_set_tag = _span_set_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>(); + + void span_set_tag_n( + ffi.Pointer span, + ffi.Pointer tag, + int tag_len, + ffi.Pointer value, + int value_len, + ) { + return _span_set_tag_n( + span, + tag, + tag_len, + value, + value_len, + ); + } + + late final _span_set_tag_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_span_set_tag_n'); + late final _span_set_tag_n = _span_set_tag_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int, + ffi.Pointer, int)>(); + + /// Removes a tag from a Span. + /// + /// The Span should not be mutated by other functions while a tag is being + /// removed from it. + void span_remove_tag( + ffi.Pointer span, + ffi.Pointer tag, + ) { + return _span_remove_tag( + span, + tag, + ); + } + + late final _span_remove_tagPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_span_remove_tag'); + late final _span_remove_tag = _span_remove_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void span_remove_tag_n( + ffi.Pointer span, + ffi.Pointer tag, + int tag_len, + ) { + return _span_remove_tag_n( + span, + tag, + tag_len, + ); + } + + late final _span_remove_tag_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Size)>>('sentry_span_remove_tag_n'); + late final _span_remove_tag_n = _span_remove_tag_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets the given key in a Span's "data" section to the given value. + /// + /// The Span should not be mutated by other functions while data is being set on + /// it. + void span_set_data( + ffi.Pointer span, + ffi.Pointer key, + sentry_value_u value, + ) { + return _span_set_data( + span, + key, + value, + ); + } + + late final _span_set_dataPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + sentry_value_u)>>('sentry_span_set_data'); + late final _span_set_data = _span_set_dataPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, sentry_value_u)>(); + + void span_set_data_n( + ffi.Pointer span, + ffi.Pointer key, + int key_len, + sentry_value_u value, + ) { + return _span_set_data_n( + span, + key, + key_len, + value, + ); + } + + late final _span_set_data_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Size, sentry_value_u)>>('sentry_span_set_data_n'); + late final _span_set_data_n = _span_set_data_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int, + sentry_value_u)>(); + + /// Removes a key from a Span's "data" section. + /// + /// The Span should not be mutated by other functions while data is being removed + /// from it. + void span_remove_data( + ffi.Pointer span, + ffi.Pointer key, + ) { + return _span_remove_data( + span, + key, + ); + } + + late final _span_remove_dataPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_span_remove_data'); + late final _span_remove_data = _span_remove_dataPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); + + void span_remove_data_n( + ffi.Pointer span, + ffi.Pointer key, + int key_len, + ) { + return _span_remove_data_n( + span, + key, + key_len, + ); + } + + late final _span_remove_data_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Size)>>('sentry_span_remove_data_n'); + late final _span_remove_data_n = _span_remove_data_nPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer, int)>(); + + /// Sets a Transaction's name. + /// + /// The Transaction should not be mutated by other functions while setting its + /// name. + void transaction_set_name( + ffi.Pointer transaction, + ffi.Pointer name, + ) { + return _transaction_set_name( + transaction, + name, + ); + } + + late final _transaction_set_namePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_transaction_set_name'); + late final _transaction_set_name = _transaction_set_namePtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + + void transaction_set_name_n( + ffi.Pointer transaction, + ffi.Pointer name, + int name_len, + ) { + return _transaction_set_name_n( + transaction, + name, + name_len, + ); + } + + late final _transaction_set_name_nPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size)>>('sentry_transaction_set_name_n'); + late final _transaction_set_name_n = _transaction_set_name_nPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer, int)>(); + + /// 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_value_u value_new_user_feedback( + ffi.Pointer uuid, + ffi.Pointer name, + ffi.Pointer email, + ffi.Pointer comments, + ) { + return _value_new_user_feedback( + uuid, + name, + email, + comments, + ); + } + + late final _value_new_user_feedbackPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer)>>('sentry_value_new_user_feedback'); + late final _value_new_user_feedback = _value_new_user_feedbackPtr.asFunction< + sentry_value_u Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer, ffi.Pointer)>(); + + sentry_value_u value_new_user_feedback_n( + ffi.Pointer uuid, + ffi.Pointer name, + int name_len, + ffi.Pointer email, + int email_len, + ffi.Pointer comments, + int comments_len, + ) { + return _value_new_user_feedback_n( + uuid, + name, + name_len, + email, + email_len, + comments, + comments_len, + ); + } + + late final _value_new_user_feedback_nPtr = _lookup< + ffi.NativeFunction< + sentry_value_u Function( + ffi.Pointer, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size, + ffi.Pointer, + ffi.Size)>>('sentry_value_new_user_feedback_n'); + late final _value_new_user_feedback_n = + _value_new_user_feedback_nPtr.asFunction< + sentry_value_u Function( + ffi.Pointer, + ffi.Pointer, + int, + ffi.Pointer, + int, + ffi.Pointer, + int)>(); + + /// Captures a manually created User Feedback and sends it to Sentry. + void capture_user_feedback( + sentry_value_u user_feedback, + ) { + return _capture_user_feedback( + user_feedback, + ); + } + + late final _capture_user_feedbackPtr = + _lookup>( + 'sentry_capture_user_feedback'); + late final _capture_user_feedback = + _capture_user_feedbackPtr.asFunction(); + + /// Sets a Span's status. + /// + /// The Span should not be mutated by other functions while setting its status. + void span_set_status( + ffi.Pointer span, + int status, + ) { + return _span_set_status( + span, + status, + ); + } + + late final _span_set_statusPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int32)>>('sentry_span_set_status'); + late final _span_set_status = _span_set_statusPtr + .asFunction, int)>(); + + /// Sets a Transaction's status. + /// + /// The Transaction should not be mutated by other functions while setting its + /// status. + void transaction_set_status( + ffi.Pointer tx, + int status, + ) { + return _transaction_set_status( + tx, + status, + ); + } + + late final _transaction_set_statusPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Int32)>>('sentry_transaction_set_status'); + late final _transaction_set_status = _transaction_set_statusPtr + .asFunction, int)>(); + + /// Iterates the distributed tracing HTTP headers for the given span. + void span_iter_headers( + ffi.Pointer span, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>> + callback, + ffi.Pointer userdata, + ) { + return _span_iter_headers( + span, + callback, + userdata, + ); + } + + late final _span_iter_headersPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>>('sentry_span_iter_headers'); + late final _span_iter_headers = _span_iter_headersPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>(); + + /// Iterates the distributed tracing HTTP headers for the given transaction. + void transaction_iter_headers( + ffi.Pointer tx, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, ffi.Pointer, + ffi.Pointer)>> + callback, + ffi.Pointer userdata, + ) { + return _transaction_iter_headers( + tx, + callback, + userdata, + ); + } + + late final _transaction_iter_headersPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>>('sentry_transaction_iter_headers'); + late final _transaction_iter_headers = + _transaction_iter_headersPtr.asFunction< + void Function( + ffi.Pointer, + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer, ffi.Pointer)>>, + ffi.Pointer)>(); + + /// Returns whether the application has crashed on the last run. + /// + /// Notes: + /// * The underlying value is set by sentry_init() - it must be called first. + /// * Call sentry_clear_crashed_last_run() to reset for the next app run. + /// + /// Possible return values: + /// 1 = the last run was a crash + /// 0 = no crash recognized + /// -1 = sentry_init() hasn't been called yet + int get_crashed_last_run() { + return _get_crashed_last_run(); + } + + late final _get_crashed_last_runPtr = + _lookup>( + 'sentry_get_crashed_last_run'); + late final _get_crashed_last_run = + _get_crashed_last_runPtr.asFunction(); + + /// Clear the status of the "crashed-last-run". You should explicitly call + /// this after sentry_init() if you're using sentry_get_crashed_last_run(). + /// Otherwise, the same information is reported on any subsequent runs. + /// + /// Notes: + /// * This doesn't change the value of sentry_get_crashed_last_run() yet. + /// However, if sentry_init() is called again, the value will change. + /// * This may only be called after sentry_init() and before sentry_close(). + /// + /// Returns 0 on success, 1 on error. + int clear_crashed_last_run() { + return _clear_crashed_last_run(); + } + + late final _clear_crashed_last_runPtr = + _lookup>( + 'sentry_clear_crashed_last_run'); + late final _clear_crashed_last_run = + _clear_crashed_last_runPtr.asFunction(); + + /// Sentry SDK version. + ffi.Pointer sdk_version() { + return _sdk_version(); + } + + late final _sdk_versionPtr = + _lookup Function()>>( + 'sentry_sdk_version'); + late final _sdk_version = + _sdk_versionPtr.asFunction Function()>(); + + /// Sentry SDK name set during build time. + /// Deprecated: Please use sentry_options_get_sdk_name instead. + ffi.Pointer sdk_name() { + return _sdk_name(); + } + + late final _sdk_namePtr = + _lookup Function()>>( + 'sentry_sdk_name'); + late final _sdk_name = + _sdk_namePtr.asFunction Function()>(); + + /// Sentry SDK User-Agent set during build time. + /// Deprecated: Please use sentry_options_get_user_agent instead. + ffi.Pointer sdk_user_agent() { + return _sdk_user_agent(); + } + + late final _sdk_user_agentPtr = + _lookup Function()>>( + 'sentry_sdk_user_agent'); + late final _sdk_user_agent = + _sdk_user_agentPtr.asFunction Function()>(); +} + +/// Represents a sentry protocol value. +/// +/// The members of this type should never be accessed. They are only here +/// so that alignment for the type can be properly determined. +/// +/// Values must be released with `sentry_value_decref`. This lowers the +/// internal refcount by one. If the refcount hits zero it's freed. Some +/// values like primitives have no refcount (like null) so operations on +/// those are no-ops. +/// +/// In addition values can be frozen. Some values like primitives are always +/// frozen but lists and dicts are not and can be frozen on demand. This +/// automatically happens for some shared values in the event payload like +/// the module list. +class sentry_value_u extends ffi.Union { + @ffi.Uint64() + external int _bits; + + @ffi.Double() + external double _double; +} + +/// Type of a sentry value. +abstract class sentry_value_type_t { + static const int SENTRY_VALUE_TYPE_NULL = 0; + static const int SENTRY_VALUE_TYPE_BOOL = 1; + static const int SENTRY_VALUE_TYPE_INT32 = 2; + static const int SENTRY_VALUE_TYPE_DOUBLE = 3; + static const int SENTRY_VALUE_TYPE_STRING = 4; + static const int SENTRY_VALUE_TYPE_LIST = 5; + static const int SENTRY_VALUE_TYPE_OBJECT = 6; +} + +/// Sentry levels for events and breadcrumbs. +abstract class sentry_level_e { + static const int SENTRY_LEVEL_DEBUG = -1; + static const int SENTRY_LEVEL_INFO = 0; + static const int SENTRY_LEVEL_WARNING = 1; + static const int SENTRY_LEVEL_ERROR = 2; + static const int SENTRY_LEVEL_FATAL = 3; +} + +/// This represents the OS dependent user context in the case of a crash, and can +/// be used to manually capture a crash. +class sentry_ucontext_s extends ffi.Opaque {} + +/// A UUID +class sentry_uuid_s extends ffi.Struct { + @ffi.Array.multi([16]) + external ffi.Array bytes; +} + +/// A Sentry Envelope. +/// +/// The Envelope is an abstract type which represents a payload being sent to +/// sentry. It can contain one or more items, typically an Event. +/// See https://develop.sentry.dev/sdk/envelopes/ +class sentry_envelope_s extends ffi.Opaque {} + +/// This represents an interface for user-defined transports. +/// +/// Transports are responsible for sending envelopes to sentry and are the last +/// step in the event pipeline. +/// +/// Envelopes will be submitted to the transport in a _fire and forget_ fashion, +/// and the transport must send those envelopes _in order_. +/// +/// A transport has the following hooks, all of which +/// take the user provided `state` as last parameter. The transport state needs +/// to be set with `sentry_transport_set_state` and typically holds handles and +/// other information that can be reused across requests. +/// +/// * `send_func`: This function will take ownership of an envelope, and is +/// responsible for freeing it via `sentry_envelope_free`. +/// * `startup_func`: This hook will be called by sentry inside of `sentry_init` +/// and instructs the transport to initialize itself. Failures will bubble up +/// to `sentry_init`. +/// * `flush_func`: Instructs the transport to flush its queue. +/// This hook receives a millisecond-resolution `timeout` parameter and should +/// return `0` if the transport queue is flushed within the timeout. +/// * `shutdown_func`: Instructs the transport to flush its queue and shut down. +/// This hook receives a millisecond-resolution `timeout` parameter and should +/// return `0` if the transport is flushed and shut down successfully. +/// In case of a non-zero return value, sentry will log an error, but continue +/// with freeing the transport. +/// * `free_func`: Frees the transports `state`. This hook might be called even +/// though `shutdown_func` returned a failure code previously. +/// +/// The transport interface might be extended in the future with hooks to flush +/// its internal queue without shutting down, and to dump its internal queue to +/// disk in case of a hard crash. +class sentry_transport_s extends ffi.Opaque {} + +/// The Sentry Client Options. +/// +/// See https://docs.sentry.io/platforms/native/configuration/ +class sentry_options_s extends ffi.Opaque {} + +/// This represents an interface for user-defined backends. +/// +/// Backends are responsible to handle crashes. They are maintained at runtime +/// via various life-cycle hooks from the sentry-core. +/// +/// At this point none of those interfaces are exposed in the API including +/// creation and destruction. The main use-case of the backend in the API at this +/// point is to disable it via `sentry_options_set_backend` at runtime before it +/// is initialized. +class sentry_backend_s extends ffi.Opaque {} + +/// The state of user consent. +abstract class sentry_user_consent_t { + static const int SENTRY_USER_CONSENT_UNKNOWN = -1; + static const int SENTRY_USER_CONSENT_GIVEN = 1; + static const int SENTRY_USER_CONSENT_REVOKED = 0; +} + +/// -- Session APIs -- +abstract class sentry_session_status_t { + static const int SENTRY_SESSION_STATUS_OK = 0; + static const int SENTRY_SESSION_STATUS_CRASHED = 1; + static const int SENTRY_SESSION_STATUS_ABNORMAL = 2; + static const int SENTRY_SESSION_STATUS_EXITED = 3; +} + +/// A sentry Transaction Context. +/// +/// See Transaction Interface under +/// https://develop.sentry.dev/sdk/performance/#new-span-and-transaction-classes +class sentry_transaction_context_s extends ffi.Opaque {} + +/// A sentry Transaction. +/// +/// See https://develop.sentry.dev/sdk/event-payloads/transaction/ +class sentry_transaction_s extends ffi.Opaque {} + +/// A sentry Span. +/// +/// See https://develop.sentry.dev/sdk/event-payloads/span/ +class sentry_span_s extends ffi.Opaque {} + +/// The status of a Span or Transaction. +/// +/// See https://develop.sentry.dev/sdk/event-payloads/span/ for documentation. +abstract class sentry_span_status_t { + /// The operation completed successfully. + /// HTTP status 100..299 + successful redirects from the 3xx range. + static const int SENTRY_SPAN_STATUS_OK = 0; + + /// The operation was cancelled (typically by the user). + static const int SENTRY_SPAN_STATUS_CANCELLED = 1; + + /// Unknown. Any non-standard HTTP status code. + /// "We do not know whether the transaction failed or succeeded." + static const int SENTRY_SPAN_STATUS_UNKNOWN = 2; + + /// Client specified an invalid argument. 4xx. + /// Note that this differs from FailedPrecondition. InvalidArgument + /// indicates arguments that are problematic regardless of the + /// state of the system. + static const int SENTRY_SPAN_STATUS_INVALID_ARGUMENT = 3; + + /// Deadline expired before operation could complete. + /// For operations that change the state of the system, this error may be + /// returned even if the operation has been completed successfully. + /// HTTP redirect loops and 504 Gateway Timeout. + static const int SENTRY_SPAN_STATUS_DEADLINE_EXCEEDED = 4; + + /// 404 Not Found. Some requested entity (file or directory) was not found. + static const int SENTRY_SPAN_STATUS_NOT_FOUND = 5; + + /// Already exists (409) + /// Some entity that we attempted to create already exists. + static const int SENTRY_SPAN_STATUS_ALREADY_EXISTS = 6; + + /// 403 Forbidden + /// The caller does not have permission to execute the specified operation. + static const int SENTRY_SPAN_STATUS_PERMISSION_DENIED = 7; + + /// 429 Too Many Requests + /// Some resource has been exhausted, perhaps a per-user quota or perhaps + /// the entire file system is out of space. + static const int SENTRY_SPAN_STATUS_RESOURCE_EXHAUSTED = 8; + + /// Operation was rejected because the system is not in a state required for + /// the operation's execution. + static const int SENTRY_SPAN_STATUS_FAILED_PRECONDITION = 9; + + /// The operation was aborted, typically due to a concurrency issue. + static const int SENTRY_SPAN_STATUS_ABORTED = 10; + + /// Operation was attempted past the valid range. + static const int SENTRY_SPAN_STATUS_OUT_OF_RANGE = 11; + + /// 501 Not Implemented + /// Operation is not implemented or not enabled. + static const int SENTRY_SPAN_STATUS_UNIMPLEMENTED = 12; + + /// Other/generic 5xx + static const int SENTRY_SPAN_STATUS_INTERNAL_ERROR = 13; + + /// 503 Service Unavailable + static const int SENTRY_SPAN_STATUS_UNAVAILABLE = 14; + + /// Unrecoverable data loss or corruption + static const int SENTRY_SPAN_STATUS_DATA_LOSS = 15; + + /// 401 Unauthorized (actually does mean unauthenticated according to RFC + /// 7235) + /// Prefer PermissionDenied if a user is logged in. + static const int SENTRY_SPAN_STATUS_UNAUTHENTICATED = 16; +} diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 71e004ecb5..3760f57072 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; @@ -16,29 +18,30 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { final SentryFlutterOptions options; @visibleForTesting - late final native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); + static late final native = + binding.SentryNative(DynamicLibrary.open('sentry.dll')); SentryNative(this.options); - Future init(SentryFlutterOptions options) async { + FutureOr init(SentryFlutterOptions options) { assert(this.options == options); if (!options.enableNativeCrashHandling) { options.logger( SentryLevel.info, 'SentryNative crash handling is disabled'); - return; + } else { + tryCatchSync("init", () { + final cOptions = createOptions(options); + final code = native.init(cOptions); + if (code != 0) { + throw StateError( + "Failed to initialize native SDK - init() exit code: $code"); + } + }); } - - tryCatchSync("init", () { - final cOptions = createOptions(options); - final code = native.init(cOptions); - if (code != 0) { - throw StateError( - "Failed to initialize native SDK - init() exit code: $code"); - } - }); } + @visibleForTesting Pointer createOptions( SentryFlutterOptions options) { final c = FreeableFactory(); @@ -63,64 +66,72 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { } } - Future close() { - throw UnimplementedError(); - } + FutureOr close() => native.close(); - Future fetchNativeAppStart() { - throw UnimplementedError(); - } + FutureOr fetchNativeAppStart() => null; - Future captureEnvelope( + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { throw UnimplementedError(); } - Future beginNativeFrames() { - throw UnimplementedError(); - } - - Future endNativeFrames(SentryId id) { - throw UnimplementedError(); - } - - Future setUser(SentryUser? user) { - throw UnimplementedError(); + FutureOr beginNativeFrames() {} + + FutureOr endNativeFrames(SentryId id) => null; + + FutureOr setUser(SentryUser? user) { + if (user == null) { + tryCatchSync('remove_user', native.remove_user); + } else { + tryCatchSync('set_user', () { + // see https://develop.sentry.dev/sdk/event-payloads/user/ + var cUser = native.value_new_object(); + user.id?.toNativeValue(cUser, "id"); + user.username?.toNativeValue(cUser, "username"); + user.email?.toNativeValue(cUser, "email"); + user.ipAddress?.toNativeValue(cUser, "ip_address"); + user.name?.toNativeValue(cUser, "name"); + // TODO + // user.data + // user.geo + native.set_user(cUser); + }); + } } - Future addBreadcrumb(Breadcrumb breadcrumb) { + FutureOr addBreadcrumb(Breadcrumb breadcrumb) { throw UnimplementedError(); } - Future clearBreadcrumbs() { + FutureOr clearBreadcrumbs() { throw UnimplementedError(); } - Future?> loadContexts() { + FutureOr?> loadContexts() { throw UnimplementedError(); } - Future setContexts(String key, dynamic value) { + FutureOr setContexts(String key, dynamic value) { throw UnimplementedError(); } - Future removeContexts(String key) { + FutureOr removeContexts(String key) { throw UnimplementedError(); } - Future setExtra(String key, dynamic value) { + FutureOr setExtra(String key, dynamic value) { throw UnimplementedError(); } - Future removeExtra(String key) { + FutureOr removeExtra(String key) { throw UnimplementedError(); } - Future setTag(String key, String value) { + FutureOr setTag(String key, String value) { throw UnimplementedError(); } - Future removeTag(String key) { + FutureOr removeTag(String key) { throw UnimplementedError(); } @@ -128,28 +139,35 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { throw UnimplementedError(); } - Future discardProfiler(SentryId traceId) { + FutureOr discardProfiler(SentryId traceId) { throw UnimplementedError(); } - Future displayRefreshRate() { + FutureOr displayRefreshRate() { throw UnimplementedError(); } - Future?> collectProfile( + FutureOr?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs) { throw UnimplementedError(); } - Future?> loadDebugImages() { + FutureOr?> loadDebugImages() { throw UnimplementedError(); } - Future pauseAppHangTracking() { - throw UnimplementedError(); - } + FutureOr pauseAppHangTracking() {} - Future resumeAppHangTracking() { - throw UnimplementedError(); + FutureOr resumeAppHangTracking() {} +} + +extension on String { + void toNativeValue(binding.sentry_value_u obj, String key) { + final cKey = key.toNativeUtf8(); + final cValue = this.toNativeUtf8(); + SentryNative.native.value_set_by_key( + obj, cKey.cast(), SentryNative.native.value_new_string(cValue.cast())); + malloc.free(cKey); + malloc.free(cValue); } } diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 002790fc32..5fcc7fa5f5 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -10,51 +10,51 @@ import 'native_frames.dart'; /// Provide typed methods to access native layer. @internal abstract class SentryNativeBinding { - Future init(SentryFlutterOptions options); + FutureOr init(SentryFlutterOptions options); - Future close(); + FutureOr close(); - Future fetchNativeAppStart(); + FutureOr fetchNativeAppStart(); - Future captureEnvelope( + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException); - Future beginNativeFrames(); + FutureOr beginNativeFrames(); - Future endNativeFrames(SentryId id); + FutureOr endNativeFrames(SentryId id); - Future setUser(SentryUser? user); + FutureOr setUser(SentryUser? user); - Future addBreadcrumb(Breadcrumb breadcrumb); + FutureOr addBreadcrumb(Breadcrumb breadcrumb); - Future clearBreadcrumbs(); + FutureOr clearBreadcrumbs(); - Future?> loadContexts(); + FutureOr?> loadContexts(); - Future setContexts(String key, dynamic value); + FutureOr setContexts(String key, dynamic value); - Future removeContexts(String key); + FutureOr removeContexts(String key); - Future setExtra(String key, dynamic value); + FutureOr setExtra(String key, dynamic value); - Future removeExtra(String key); + FutureOr removeExtra(String key); - Future setTag(String key, String value); + FutureOr setTag(String key, String value); - Future removeTag(String key); + FutureOr removeTag(String key); int? startProfiler(SentryId traceId); - Future discardProfiler(SentryId traceId); + FutureOr discardProfiler(SentryId traceId); - Future displayRefreshRate(); + FutureOr displayRefreshRate(); - Future?> collectProfile( + FutureOr?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs); - Future?> loadDebugImages(); + FutureOr?> loadDebugImages(); - Future pauseAppHangTracking(); + FutureOr pauseAppHangTracking(); - Future resumeAppHangTracking(); + FutureOr resumeAppHangTracking(); } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index c688f9862b..76c3780556 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -247,30 +247,30 @@ mixin SentryFlutter { /// Pauses the app hang tracking. /// Only for iOS and macOS. - static Future pauseAppHangTracking() { + static Future pauseAppHangTracking() async { if (_native == null) { // ignore: invalid_use_of_internal_member Sentry.currentHub.options.logger( SentryLevel.debug, 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the pauseAppHangTracking API.', ); - return Future.value(); + } else { + await _native!.pauseAppHangTracking(); } - return _native!.pauseAppHangTracking(); } /// Resumes the app hang tracking. /// Only for iOS and macOS - static Future resumeAppHangTracking() { + static Future resumeAppHangTracking() async { if (_native == null) { // ignore: invalid_use_of_internal_member Sentry.currentHub.options.logger( SentryLevel.debug, 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the resumeAppHangTracking API.', ); - return Future.value(); + } else { + await _native!.resumeAppHangTracking(); } - return _native!.resumeAppHangTracking(); } @internal diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 0f94f80bec..9f81dcce1d 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -4,6 +4,7 @@ library flutter_test; import 'dart:async'; import 'dart:io'; +import 'package:ffi/ffi.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry/src/platform/platform.dart' as platform; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -40,7 +41,6 @@ void main() { // Compile sentry-native using CMake, as if it was part of a Flutter app. final cmakeBuildDir = '$nativeTestRoot/build'; final cmakeConfDir = '$nativeTestRoot/conf'; - final cmakeOutputDir = '$nativeTestRoot/out'; Directory(cmakeConfDir).createSync(recursive: true); File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' cmake_minimum_required(VERSION 3.14) @@ -64,6 +64,8 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled sut = createBinding(options) as SentryNative; }); + tearDown(() => sut.close()); + test('options', () { options ..debug = true @@ -71,14 +73,33 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled ..release = 'foo@bar+1' ..enableAutoSessionTracking = true ..dist = 'distfoo' - ..diagnosticLevel = SentryLevel.error ..maxBreadcrumbs = 42; final cOptions = sut.createOptions(options); try { - // sut.native.options_get_dsn(cOptions) + expect( + SentryNative.native + .options_get_dsn(cOptions) + .cast() + .toDartString(), + fakeDsn); + expect( + SentryNative.native + .options_get_environment(cOptions) + .cast() + .toDartString(), + 'foo'); + expect( + SentryNative.native + .options_get_release(cOptions) + .cast() + .toDartString(), + 'foo@bar+1'); + expect( + SentryNative.native.options_get_auto_session_tracking(cOptions), 1); + expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); } finally { - sut.native.options_free(cOptions); + SentryNative.native.options_free(cOptions); } }); @@ -87,48 +108,30 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled await sut.init(options); }); - // test('beginNativeFrames', () async { - // when(channel.invokeMethod('beginNativeFrames')) - // .thenAnswer((realInvocation) async {}); - // await sut.beginNativeFrames(); - - // verify(channel.invokeMethod('beginNativeFrames')); - // }); - - // test('endNativeFrames', () async { - // final sentryId = SentryId.empty(); + test('app start', () { + expect(sut.fetchNativeAppStart(), null); + }); - // when(channel - // .invokeMethod('endNativeFrames', {'id': sentryId.toString()})) - // .thenAnswer((_) async => { - // 'totalFrames': 3, - // 'slowFrames': 2, - // 'frozenFrames': 1, - // }); + test('frames tracking', () { + sut.beginNativeFrames(); + expect(sut.endNativeFrames(SentryId.newId()), null); + }); - // final actual = await sut.endNativeFrames(sentryId); + test('hang tracking', () { + sut.pauseAppHangTracking(); + sut.resumeAppHangTracking(); + }); - // expect(actual?.totalFrames, 3); - // expect(actual?.slowFrames, 2); - // expect(actual?.frozenFrames, 1); - // }); + // test('setUser', () async { + // final user = SentryUser( + // id: "fixture-id", + // data: {'object': Object()}, + // ); - // test('setUser', () async { - // final user = SentryUser( - // id: "fixture-id", - // data: {'object': Object()}, - // ); - // final normalizedUser = user.copyWith( - // data: MethodChannelHelper.normalizeMap(user.data), - // ); - // when(channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})) - // .thenAnswer((_) => Future.value()); + // await sut.setUser(user); - // await sut.setUser(user); - - // verify( - // channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})); - // }); + // verify(channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})); + // }); // test('addBreadcrumb', () async { // final breadcrumb = Breadcrumb( @@ -318,22 +321,4 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled // expect(data?.map((v) => v.toJson()), json); // }); - - // test('pauseAppHangTracking', () async { - // when(channel.invokeMethod('pauseAppHangTracking')) - // .thenAnswer((_) => Future.value()); - - // await sut.pauseAppHangTracking(); - - // verify(channel.invokeMethod('pauseAppHangTracking')); - // }); - - // test('resumeAppHangTracking', () async { - // when(channel.invokeMethod('resumeAppHangTracking')) - // .thenAnswer((_) => Future.value()); - - // await sut.resumeAppHangTracking(); - - // verify(channel.invokeMethod('resumeAppHangTracking')); - // }); } From cac80c39d42fb49b082203ee8a2750d91f1dd700 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 08:33:05 +0200 Subject: [PATCH 06/55] improve native tests --- flutter/ffi-native.yaml | 2 + .../sentry_native/sentry_native_test.dart | 103 ++++++++++++------ 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index 33f127fa58..7a23983619 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -19,6 +19,8 @@ functions: - sentry_options_set_dist - sentry_options_set_max_breadcrumbs # For tests only: + - sentry_sdk_version + - sentry_sdk_name - sentry_options_free - sentry_options_get_dsn - sentry_options_get_debug diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 9f81dcce1d..a42ed4720b 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -13,45 +13,13 @@ import 'package:sentry_flutter/src/native/factory.dart'; import '../mocks.dart'; -/// Runs [command] with command's stdout and stderr being forwrarded to -/// test runner's respective streams. It buffers stdout and returns it. -/// -/// Returns [_CommandResult] with exitCode and stdout as a single sting -Future _exec(String executable, List arguments) async { - final process = await Process.start(executable, arguments); - - // forward standard streams - unawaited(stderr.addStream(process.stderr)); - unawaited(stdout.addStream(process.stdout)); - - int exitCode = await process.exitCode; - if (exitCode != 0) { - throw Exception( - "$executable ${arguments.join(' ')} failed with exit code $exitCode"); - } -} - void main() { - const nativeTestRoot = 'temp/native-test'; if (Directory.current.path.endsWith('/test')) { Directory.current = Directory.current.parent; } setUpAll(() async { - // Compile sentry-native using CMake, as if it was part of a Flutter app. - final cmakeBuildDir = '$nativeTestRoot/build'; - final cmakeConfDir = '$nativeTestRoot/conf'; - Directory(cmakeConfDir).createSync(recursive: true); - File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' -cmake_minimum_required(VERSION 3.14) -project(sentry-native-flutter-test) -add_subdirectory(../../../${platform.instance.operatingSystem} plugin) -add_library(\${CMAKE_PROJECT_NAME} INTERFACE) -target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled_libraries}) -'''); - await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); - await _exec('cmake', ['--build', cmakeBuildDir]); - Directory.current = '$cmakeBuildDir/_deps/sentry-native-build/Debug/'; + Directory.current = await _buildSentryNative('temp/native-test'); }); late SentryNative sut; @@ -103,6 +71,17 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled } }); + test('SDK version', () { + expect(_configuredSentryNativeVersion.length, greaterThanOrEqualTo(5)); + expect(SentryNative.native.sdk_version().cast().toDartString(), + _configuredSentryNativeVersion); + }); + + test('SDK name', () { + expect(SentryNative.native.sdk_name().cast().toDartString(), + 'sentry.native.flutter'); + }); + test('init', () async { // There's nothing we can check here - just that it doesn't crash. await sut.init(options); @@ -322,3 +301,61 @@ target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled // expect(data?.map((v) => v.toJson()), json); // }); } + +/// Runs [command] with command's stdout and stderr being forwrarded to +/// test runner's respective streams. It buffers stdout and returns it. +/// +/// Returns [_CommandResult] with exitCode and stdout as a single sting +Future _exec(String executable, List arguments) async { + final process = await Process.start(executable, arguments); + + // forward standard streams + unawaited(stderr.addStream(process.stderr)); + unawaited(stdout.addStream(process.stdout)); + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "$executable ${arguments.join(' ')} failed with exit code $exitCode"); + } +} + +/// Compile sentry-native using CMake, as if it was part of a Flutter app. +/// Returns the directory containing built libraries +Future _buildSentryNative(String nativeTestRoot) async { + final cmakeBuildDir = '$nativeTestRoot/build'; + final cmakeConfDir = '$nativeTestRoot/conf'; + final buildOutputDir = '$cmakeBuildDir/_deps/sentry-native-build/Debug/'; + + if (!_builtVersionIsExpected(buildOutputDir)) { + Directory(cmakeConfDir).createSync(recursive: true); + File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' +cmake_minimum_required(VERSION 3.14) +project(sentry-native-flutter-test) +add_subdirectory(../../../${platform.instance.operatingSystem} plugin) +add_library(\${CMAKE_PROJECT_NAME} INTERFACE) +target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled_libraries}) +'''); + await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); + await _exec('cmake', ['--build', cmakeBuildDir]); + } + return buildOutputDir; +} + +bool _builtVersionIsExpected(String buildOutputDir) { + final buildCmake = File('$buildOutputDir/../sentry-config-version.cmake'); + if (!buildCmake.existsSync()) return false; + + if (!buildCmake + .readAsStringSync() + .contains('set(PACKAGE_VERSION "$_configuredSentryNativeVersion")')) { + return false; + } + + return File('$buildOutputDir/sentry.dll').existsSync(); +} + +late final _configuredSentryNativeVersion = File('sentry-native/CMakeCache.txt') + .readAsLinesSync() + .map((line) => line.startsWith('version=') ? line.substring(8) : null) + .firstWhere((line) => line != null)!; From dd8d1e64ed59f51eafafdc26b89e4554b5260df8 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 12:10:15 +0200 Subject: [PATCH 07/55] more native binding calls --- flutter/lib/src/native/c/sentry_native.dart | 181 +++++++++++--- .../sentry_native/sentry_native_test.dart | 222 ++++++------------ 2 files changed, 215 insertions(+), 188 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 3760f57072..b8beb38d33 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -23,6 +23,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { SentryNative(this.options); + void _logNotSupported(String operation) => options.logger( + SentryLevel.debug, 'SentryNative: $operation is not supported'); + FutureOr init(SentryFlutterOptions options) { assert(this.options == options); @@ -84,72 +87,101 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { tryCatchSync('remove_user', native.remove_user); } else { tryCatchSync('set_user', () { - // see https://develop.sentry.dev/sdk/event-payloads/user/ - var cUser = native.value_new_object(); - user.id?.toNativeValue(cUser, "id"); - user.username?.toNativeValue(cUser, "username"); - user.email?.toNativeValue(cUser, "email"); - user.ipAddress?.toNativeValue(cUser, "ip_address"); - user.name?.toNativeValue(cUser, "name"); - // TODO - // user.data - // user.geo + var cUser = user.toJson().toNativeValue(options.logger); native.set_user(cUser); }); } } FutureOr addBreadcrumb(Breadcrumb breadcrumb) { - throw UnimplementedError(); + tryCatchSync('add_breadcrumb', () { + var cBreadcrumb = breadcrumb.toJson().toNativeValue(options.logger); + native.add_breadcrumb(cBreadcrumb); + }); } FutureOr clearBreadcrumbs() { - throw UnimplementedError(); + _logNotSupported('clearing breadcrumbs'); } FutureOr?> loadContexts() { - throw UnimplementedError(); + _logNotSupported('loading contexts'); + return null; } FutureOr setContexts(String key, dynamic value) { - throw UnimplementedError(); + tryCatchSync('set_context', () { + final cValue = dynamicToNativeValue(value, options.logger); + if (cValue != null) { + final cKey = key.toNativeUtf8(); + native.set_context(cKey.cast(), cValue); + malloc.free(cKey); + } else { + options.logger(SentryLevel.warning, + 'SentryNative: failed to set context $key - value couldn\'t be converted to native'); + } + }); } FutureOr removeContexts(String key) { - throw UnimplementedError(); + tryCatchSync('remove_context', () { + final cKey = key.toNativeUtf8(); + native.remove_context(cKey.cast()); + malloc.free(cKey); + }); } FutureOr setExtra(String key, dynamic value) { - throw UnimplementedError(); + tryCatchSync('set_extra', () { + final cValue = dynamicToNativeValue(value, options.logger); + if (cValue != null) { + final cKey = key.toNativeUtf8(); + native.set_extra(cKey.cast(), cValue); + malloc.free(cKey); + } else { + options.logger(SentryLevel.warning, + 'SentryNative: failed to set extra $key - value couldn\'t be converted to native'); + } + }); } FutureOr removeExtra(String key) { - throw UnimplementedError(); + tryCatchSync('remove_extra', () { + final cKey = key.toNativeUtf8(); + native.remove_extra(cKey.cast()); + malloc.free(cKey); + }); } FutureOr setTag(String key, String value) { - throw UnimplementedError(); + tryCatchSync('set_tag', () { + final c = FreeableFactory(); + native.set_tag(c.str(key), c.str(value)); + c.freeAll(); + }); } FutureOr removeTag(String key) { - throw UnimplementedError(); + tryCatchSync('set_tag', () { + final cKey = key.toNativeUtf8(); + native.remove_tag(cKey.cast()); + malloc.free(cKey); + }); } - int? startProfiler(SentryId traceId) { - throw UnimplementedError(); - } + int? startProfiler(SentryId traceId) => + throw UnsupportedError("Not supported on this platform"); - FutureOr discardProfiler(SentryId traceId) { - throw UnimplementedError(); - } - - FutureOr displayRefreshRate() { - throw UnimplementedError(); - } + FutureOr discardProfiler(SentryId traceId) => + throw UnsupportedError("Not supported on this platform"); FutureOr?> collectProfile( - SentryId traceId, int startTimeNs, int endTimeNs) { - throw UnimplementedError(); + SentryId traceId, int startTimeNs, int endTimeNs) => + throw UnsupportedError("Not supported on this platform"); + + FutureOr displayRefreshRate() { + _logNotSupported('collecting display refresh rate'); + return null; } FutureOr?> loadDebugImages() { @@ -161,13 +193,90 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr resumeAppHangTracking() {} } -extension on String { - void toNativeValue(binding.sentry_value_u obj, String key) { +extension on binding.sentry_value_u { + void setNativeValue(String key, binding.sentry_value_u? value) { final cKey = key.toNativeUtf8(); - final cValue = this.toNativeUtf8(); - SentryNative.native.value_set_by_key( - obj, cKey.cast(), SentryNative.native.value_new_string(cValue.cast())); + if (value == null) { + SentryNative.native.value_remove_by_key(this, cKey.cast()); + } else { + SentryNative.native.value_set_by_key(this, cKey.cast(), value); + } malloc.free(cKey); + } +} + +binding.sentry_value_u? dynamicToNativeValue( + dynamic value, SentryLogger logger) { + if (value is String) { + return value.toNativeValue(); + } else if (value is int) { + return value.toNativeValue(); + } else if (value is double) { + return value.toNativeValue(); + } else if (value is bool) { + return value.toNativeValue(); + } else if (value is Map) { + return value.toNativeValue(logger); + } else if (value is List) { + return value.toNativeValue(logger); + } else if (value == null) { + return SentryNative.native.value_new_null(); + } else { + logger(SentryLevel.warning, + 'SentryNative: unsupported data for for conversion: ${value.runtimeType} ($value)'); + return null; + } +} + +extension on String { + binding.sentry_value_u toNativeValue() { + final cValue = this.toNativeUtf8(); + final result = SentryNative.native.value_new_string(cValue.cast()); malloc.free(cValue); + return result; + } +} + +extension on int { + binding.sentry_value_u toNativeValue() { + if (this > 0x7FFFFFFF) { + return this.toString().toNativeValue(); + } else { + return SentryNative.native.value_new_int32(this); + } + } +} + +extension on double { + binding.sentry_value_u toNativeValue() => + SentryNative.native.value_new_double(this); +} + +extension on bool { + binding.sentry_value_u toNativeValue() => + SentryNative.native.value_new_bool(this ? 1 : 0); +} + +extension on Map { + binding.sentry_value_u toNativeValue(SentryLogger logger) { + final cObject = SentryNative.native.value_new_object(); + for (final entry in entries) { + final cValue = dynamicToNativeValue(entry.value, logger); + cObject.setNativeValue(entry.key, cValue); + } + return cObject; + } +} + +extension on List { + binding.sentry_value_u toNativeValue(SentryLogger logger) { + final cObject = SentryNative.native.value_new_list(); + for (final value in this) { + final cValue = dynamicToNativeValue(value, logger); + if (cValue != null) { + SentryNative.native.value_append(cObject, cValue); + } + } + return cObject; } } diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index a42ed4720b..8e4b46415f 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -101,156 +101,85 @@ void main() { sut.resumeAppHangTracking(); }); - // test('setUser', () async { - // final user = SentryUser( - // id: "fixture-id", - // data: {'object': Object()}, - // ); - - // await sut.setUser(user); - - // verify(channel.invokeMethod('setUser', {'user': normalizedUser.toJson()})); - // }); - - // test('addBreadcrumb', () async { - // final breadcrumb = Breadcrumb( - // data: {'object': Object()}, - // ); - // final normalizedBreadcrumb = breadcrumb.copyWith( - // data: MethodChannelHelper.normalizeMap(breadcrumb.data)); - - // when(channel.invokeMethod( - // 'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()})) - // .thenAnswer((_) => Future.value()); - - // await sut.addBreadcrumb(breadcrumb); - - // verify(channel.invokeMethod( - // 'addBreadcrumb', {'breadcrumb': normalizedBreadcrumb.toJson()})); - // }); - - // test('clearBreadcrumbs', () async { - // when(channel.invokeMethod('clearBreadcrumbs')) - // .thenAnswer((_) => Future.value()); - - // await sut.clearBreadcrumbs(); - - // verify(channel.invokeMethod('clearBreadcrumbs')); - // }); - - // test('setContexts', () async { - // final value = {'object': Object()}; - // final normalizedValue = MethodChannelHelper.normalize(value); - // when(channel.invokeMethod('setContexts', { - // 'key': 'fixture-key', - // 'value': normalizedValue - // })).thenAnswer((_) => Future.value()); - - // await sut.setContexts('fixture-key', value); - - // verify(channel.invokeMethod( - // 'setContexts', {'key': 'fixture-key', 'value': normalizedValue})); - // }); - - // test('removeContexts', () async { - // when(channel.invokeMethod('removeContexts', {'key': 'fixture-key'})) - // .thenAnswer((_) => Future.value()); - - // await sut.removeContexts('fixture-key'); - - // verify(channel.invokeMethod('removeContexts', {'key': 'fixture-key'})); - // }); - - // test('setExtra', () async { - // final value = {'object': Object()}; - // final normalizedValue = MethodChannelHelper.normalize(value); - // when(channel.invokeMethod( - // 'setExtra', {'key': 'fixture-key', 'value': normalizedValue})) - // .thenAnswer((_) => Future.value()); - - // await sut.setExtra('fixture-key', value); - - // verify(channel.invokeMethod( - // 'setExtra', {'key': 'fixture-key', 'value': normalizedValue})); - // }); - - // test('removeExtra', () async { - // when(channel.invokeMethod('removeExtra', {'key': 'fixture-key'})) - // .thenAnswer((_) => Future.value()); - - // await sut.removeExtra('fixture-key'); - - // verify(channel.invokeMethod('removeExtra', {'key': 'fixture-key'})); - // }); + test('setUser', () async { + final user = SentryUser( + id: "fixture-id", + username: 'username', + email: 'mail@domain.tld', + ipAddress: '1.2.3.4', + name: 'User Name', + data: { + 'str': 'foo-bar', + 'double': 1.0, + 'int': 1, + 'int64': 0x7FFFFFFF + 1, + 'boo': true, + 'inner-map': {'str': 'inner'}, + 'unsupported': Object() + }, + ); + + await sut.setUser(user); + }); - // test('setTag', () async { - // when(channel.invokeMethod( - // 'setTag', {'key': 'fixture-key', 'value': 'fixture-value'})) - // .thenAnswer((_) => Future.value()); + test('addBreadcrumb', () async { + final breadcrumb = Breadcrumb( + type: 'type', + message: 'message', + category: 'category', + ); + await sut.addBreadcrumb(breadcrumb); + }); - // await sut.setTag('fixture-key', 'fixture-value'); + test('clearBreadcrumbs', () async { + await sut.clearBreadcrumbs(); + }); - // verify(channel.invokeMethod( - // 'setTag', {'key': 'fixture-key', 'value': 'fixture-value'})); - // }); + test('displayRefreshRate', () async { + expect(sut.displayRefreshRate(), isNull); + }); - // test('removeTag', () async { - // when(channel.invokeMethod('removeTag', {'key': 'fixture-key'})) - // .thenAnswer((_) => Future.value()); + test('setContexts', () async { + final value = {'object': Object()}; + await sut.setContexts('fixture-key', value); + }); - // await sut.removeTag('fixture-key'); + test('removeContexts', () async { + await sut.removeContexts('fixture-key'); + }); - // verify(channel.invokeMethod('removeTag', {'key': 'fixture-key'})); - // }); + test('setExtra', () async { + final value = {'object': Object()}; + await sut.setExtra('fixture-key', value); + }); - // test('startProfiler', () { - // late Matcher matcher; - // if (mockPlatform.isAndroid) { - // matcher = throwsUnsupportedError; - // } else if (mockPlatform.isIOS || mockPlatform.isMacOS) { - // if (platform.instance.isMacOS) { - // matcher = throwsA(predicate((e) => - // e is Exception && - // e.toString().contains('Failed to load Objective-C class'))); - // } else { - // matcher = throwsA(predicate((e) => - // e is ArgumentError && - // e.toString().contains('Failed to lookup symbol'))); - // } - // } - // expect(() => sut.startProfiler(SentryId.newId()), matcher); + test('removeExtra', () async { + await sut.removeExtra('fixture-key'); + }); - // verifyZeroInteractions(channel); - // }); + test('setTag', () async { + await sut.setTag('fixture-key', 'fixture-value'); + }); - // test('discardProfiler', () async { - // final traceId = SentryId.newId(); - // when(channel.invokeMethod('discardProfiler', traceId.toString())) - // .thenAnswer((_) async {}); + test('removeTag', () async { + await sut.removeTag('fixture-key'); + }); - // await sut.discardProfiler(traceId); + test('startProfiler', () { + expect(() => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); + }); - // verify(channel.invokeMethod('discardProfiler', traceId.toString())); - // }); + test('discardProfiler', () async { + expect(() => sut.discardProfiler(SentryId.newId()), throwsUnsupportedError); + }); - // test('collectProfile', () async { - // final traceId = SentryId.newId(); - // const startTime = 42; - // const endTime = 50; - // when(channel.invokeMethod('collectProfile', { - // 'traceId': traceId.toString(), - // 'startTime': startTime, - // 'endTime': endTime, - // })).thenAnswer((_) async => {}); - - // await sut.collectProfile(traceId, startTime, endTime); - - // verify(channel.invokeMethod('collectProfile', { - // 'traceId': traceId.toString(), - // 'startTime': startTime, - // 'endTime': endTime, - // })); - // }); + test('collectProfile', () async { + final traceId = SentryId.newId(); + const startTime = 42; + const endTime = 50; + expect(() => sut.collectProfile(traceId, startTime, endTime), + throwsUnsupportedError); + }); // test('captureEnvelope', () async { // final data = Uint8List.fromList([1, 2, 3]); @@ -265,20 +194,9 @@ void main() { // expect(captured, data); // }); - // test('loadContexts', () async { - // when(channel.invokeMethod('loadContexts')) - // .thenAnswer((invocation) async => { - // 'foo': [1, 2, 3], - // 'bar': {'a': 'b'}, - // }); - - // final data = await sut.loadContexts(); - - // expect(data, { - // 'foo': [1, 2, 3], - // 'bar': {'a': 'b'}, - // }); - // }); + test('loadContexts', () async { + expect(await sut.loadContexts(), isNull); + }); // test('loadDebugImages', () async { // final json = [ From 50fd16ce90841a2e93f8e762466ef9a4055f7887 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 13:59:41 +0200 Subject: [PATCH 08/55] load debug image list --- flutter/lib/src/native/c/sentry_native.dart | 64 ++++++++++++++++++- .../sentry_native/sentry_native_test.dart | 36 +++++------ 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index b8beb38d33..f5c26d1187 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -184,9 +184,32 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return null; } - FutureOr?> loadDebugImages() { - throw UnimplementedError(); - } + FutureOr?> loadDebugImages() => + tryCatchSync('get_module_list', () { + final cImages = native.get_modules_list(); + try { + if (native.value_get_type(cImages) != + binding.sentry_value_type_t.SENTRY_VALUE_TYPE_LIST) { + return null; + } + + return List.generate(native.value_get_length(cImages), + (index) { + final cImage = native.value_get_by_index(cImages, index); + return DebugImage( + type: cImage.get('type').castPrimitive(options.logger) ?? '', + imageAddr: cImage.get('image_addr').castPrimitive(options.logger), + imageSize: cImage.get('image_size').castPrimitive(options.logger), + codeFile: cImage.get('code_file').castPrimitive(options.logger), + debugId: cImage.get('debug_id').castPrimitive(options.logger), + debugFile: cImage.get('debug_file').castPrimitive(options.logger), + codeId: cImage.get('code_id').castPrimitive(options.logger), + ); + }); + } finally { + native.value_decref(cImages); + } + }); FutureOr pauseAppHangTracking() {} @@ -203,6 +226,41 @@ extension on binding.sentry_value_u { } malloc.free(cKey); } + + binding.sentry_value_u get(String key) { + final cKey = key.toNativeUtf8(); + try { + return SentryNative.native.value_get_by_key(this, cKey.cast()); + } finally { + malloc.free(cKey); + } + } + + T? castPrimitive(SentryLogger logger) { + if (SentryNative.native.value_is_null(this) == 1) { + return null; + } + final type = SentryNative.native.value_get_type(this); + switch (type) { + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_NULL: + return null; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_BOOL: + return (SentryNative.native.value_is_true(this) == 1) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_INT32: + return SentryNative.native.value_as_int32(this) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_DOUBLE: + return SentryNative.native.value_as_double(this) as T; + case binding.sentry_value_type_t.SENTRY_VALUE_TYPE_STRING: + return SentryNative.native + .value_as_string(this) + .cast() + .toDartString() as T; + default: + logger(SentryLevel.warning, + 'SentryNative: cannot read native value type: $type'); + return null; + } + } } binding.sentry_value_u? dynamicToNativeValue( diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 8e4b46415f..b00985aea8 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -198,26 +198,22 @@ void main() { expect(await sut.loadContexts(), isNull); }); - // test('loadDebugImages', () async { - // final json = [ - // { - // 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', - // 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', - // 'image_addr': '0x6f80b000', - // 'image_size': 3092480, - // 'type': 'elf', - // 'debug_id': 'e77c5713-5311-28c2-ecf0-eb73fc39f450', - // 'debug_file': 'test' - // } - // ]; - - // when(channel.invokeMethod('loadImageList')) - // .thenAnswer((invocation) async => json); - - // final data = await sut.loadDebugImages(); - - // expect(data?.map((v) => v.toJson()), json); - // }); + test('loadDebugImages', () async { + final list = await sut.loadDebugImages(); + expect(list, isNotEmpty); + expect(list![0].type, 'pe'); + expect(list[0].debugId!.length, greaterThan(30)); + expect(list[0].debugFile, isNotEmpty); + expect(list[0].imageSize, greaterThan(0)); + expect(list[0].imageAddr, startsWith('0x')); + expect(list[0].imageAddr?.length, greaterThan(2)); + expect(list[0].codeId!.length, greaterThan(10)); + expect(list[0].codeFile, isNotEmpty); + expect( + File(list[0].codeFile!), + (File file) => file.existsSync(), + ); + }); } /// Runs [command] with command's stdout and stderr being forwrarded to From 852a30610f3de87c6edd1bccaff98cfa81ba08d1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 17:16:16 +0200 Subject: [PATCH 09/55] add native feature support flags --- flutter/lib/src/native/c/sentry_native.dart | 6 +++++- flutter/lib/src/native/sentry_native_binding.dart | 4 ++++ flutter/lib/src/native/sentry_native_channel.dart | 4 ++++ flutter/lib/src/sentry_flutter.dart | 8 ++++++-- flutter/test/sentry_flutter_test.dart | 4 ++-- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index f5c26d1187..82951450b7 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -73,9 +73,11 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr fetchNativeAppStart() => null; + bool get supportsCaptureEnvelope => false; + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { - throw UnimplementedError(); + throw UnsupportedError('$SentryNative.captureEnvelope() is not suppurted'); } FutureOr beginNativeFrames() {} @@ -104,6 +106,8 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { _logNotSupported('clearing breadcrumbs'); } + bool get supportsLoadContexts => false; + FutureOr?> loadContexts() { _logNotSupported('loading contexts'); return null; diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 5fcc7fa5f5..ff7866c228 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -16,6 +16,8 @@ abstract class SentryNativeBinding { FutureOr fetchNativeAppStart(); + bool get supportsCaptureEnvelope; + FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException); @@ -29,6 +31,8 @@ abstract class SentryNativeBinding { FutureOr clearBreadcrumbs(); + bool get supportsLoadContexts; + FutureOr?> loadContexts(); FutureOr setContexts(String key, dynamic value); diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 623111bb9e..d0e58405d4 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -80,6 +80,8 @@ class SentryNativeChannel return (json != null) ? NativeAppStart.fromJson(json) : null; } + bool get supportsCaptureEnvelope => true; + @override Future captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { @@ -87,6 +89,8 @@ class SentryNativeChannel 'captureEnvelope', [envelopeData, containsUnhandledException]); } + bool get supportsLoadContexts => true; + @override Future?> loadContexts() => _channel.invokeMapMethod('loadContexts'); diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 76c3780556..021509dfb4 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -125,7 +125,9 @@ mixin SentryFlutter { // Not all platforms have a native integration. if (_native != null) { - options.transport = FileSystemTransport(_native!, options); + if (_native!.supportsCaptureEnvelope) { + options.transport = FileSystemTransport(_native!, options); + } options.addScopeObserver(NativeScopeObserver(_native!)); } @@ -179,7 +181,9 @@ mixin SentryFlutter { final native = _native; if (native != null) { integrations.add(NativeSdkIntegration(native)); - integrations.add(LoadContextsIntegration(native)); + if (native.supportsLoadContexts) { + integrations.add(LoadContextsIntegration(native)); + } integrations.add(LoadImageListIntegration(native)); } diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 1d78b947d1..2f94e7ab91 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -243,7 +243,7 @@ void main() { testScopeObserver( options: sentryFlutterOptions!, - expectedHasNativeScopeObserver: false); + expectedHasNativeScopeObserver: true); testConfiguration( integrations: integrations, @@ -264,7 +264,7 @@ void main() { beforeIntegration: WidgetsFlutterBindingIntegration, afterIntegration: OnErrorIntegration); - expect(SentryFlutter.native, isNull); + expect(SentryFlutter.native, isNotNull); expect(Sentry.currentHub.profilerFactory, isNull); await Sentry.close(); From db797f9bb22e1021f8076b4a3208cc265e82d9bc Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 17:23:32 +0200 Subject: [PATCH 10/55] clean --- flutter/ffi-native.yaml | 34 +- flutter/lib/src/native/c/binding.dart | 4668 ++----------------------- 2 files changed, 347 insertions(+), 4355 deletions(-) diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index 7a23983619..0e5b3c0e5a 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -8,8 +8,8 @@ headers: exclude-all-by-default: true functions: include: - - sentry_.* - sentry_init + - sentry_close - sentry_options_new - sentry_options_set_dsn - sentry_options_set_debug @@ -18,6 +18,38 @@ functions: - sentry_options_set_auto_session_tracking - sentry_options_set_dist - sentry_options_set_max_breadcrumbs + - sentry_set_user + - sentry_remove_user + - sentry_add_breadcrumb + - sentry_set_context + - sentry_remove_context + - sentry_set_extra + - sentry_remove_extra + - sentry_set_tag + - sentry_remove_tag + - sentry_get_modules_list + - sentry_value_get_type + - sentry_value_get_length + - sentry_value_get_by_index + - sentry_value_decref + - sentry_value_set_by_key + - sentry_value_get_by_key + - sentry_value_remove_by_key + - sentry_value_is_null + - sentry_value_is_true + - sentry_value_new_null + - sentry_value_new_int32 + - sentry_value_new_double + - sentry_value_new_string + - sentry_value_new_list + - sentry_value_new_object + - sentry_value_new_bool + - sentry_value_append + - sentry_value_as_int32 + - sentry_value_as_double + - sentry_value_as_string + - sentry_value_as_list + - sentry_value_as_object # For tests only: - sentry_sdk_version - sentry_sdk_name diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index 4fe981a43b..3d2af5026d 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -20,51 +20,6 @@ class SentryNative { lookup) : _lookup = lookup; - /// Allocates memory with the underlying allocator. - ffi.Pointer malloc( - int size, - ) { - return _malloc( - size, - ); - } - - late final _mallocPtr = - _lookup Function(ffi.Size)>>( - 'sentry_malloc'); - late final _malloc = - _mallocPtr.asFunction Function(int)>(); - - /// Releases memory allocated from the underlying allocator. - void free( - ffi.Pointer ptr, - ) { - return _free( - ptr, - ); - } - - late final _freePtr = - _lookup)>>( - 'sentry_free'); - late final _free = - _freePtr.asFunction)>(); - - /// Increments the reference count on the value. - void value_incref( - sentry_value_u value, - ) { - return _value_incref( - value, - ); - } - - late final _value_increfPtr = - _lookup>( - 'sentry_value_incref'); - late final _value_incref = - _value_increfPtr.asFunction(); - /// Decrements the reference count on the value. void value_decref( sentry_value_u value, @@ -80,51 +35,6 @@ class SentryNative { late final _value_decref = _value_decrefPtr.asFunction(); - /// Returns the refcount of a value. - int value_refcount( - sentry_value_u value, - ) { - return _value_refcount( - value, - ); - } - - late final _value_refcountPtr = - _lookup>( - 'sentry_value_refcount'); - late final _value_refcount = - _value_refcountPtr.asFunction(); - - /// Freezes a value. - void value_freeze( - sentry_value_u value, - ) { - return _value_freeze( - value, - ); - } - - late final _value_freezePtr = - _lookup>( - 'sentry_value_freeze'); - late final _value_freeze = - _value_freezePtr.asFunction(); - - /// Checks if a value is frozen. - int value_is_frozen( - sentry_value_u value, - ) { - return _value_is_frozen( - value, - ); - } - - late final _value_is_frozenPtr = - _lookup>( - 'sentry_value_is_frozen'); - late final _value_is_frozen = - _value_is_frozenPtr.asFunction(); - /// Creates a null value. sentry_value_u value_new_null() { return _value_new_null(); @@ -196,23 +106,6 @@ class SentryNative { late final _value_new_string = _value_new_stringPtr .asFunction)>(); - sentry_value_u value_new_string_n( - ffi.Pointer value, - int value_len, - ) { - return _value_new_string_n( - value, - value_len, - ); - } - - late final _value_new_string_nPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer, ffi.Size)>>('sentry_value_new_string_n'); - late final _value_new_string_n = _value_new_string_nPtr - .asFunction, int)>(); - /// Creates a new list value. sentry_value_u value_new_list() { return _value_new_list(); @@ -273,28 +166,6 @@ class SentryNative { late final _value_set_by_key = _value_set_by_keyPtr.asFunction< int Function(sentry_value_u, ffi.Pointer, sentry_value_u)>(); - int value_set_by_key_n( - sentry_value_u value, - ffi.Pointer k, - int k_len, - sentry_value_u v, - ) { - return _value_set_by_key_n( - value, - k, - k_len, - v, - ); - } - - late final _value_set_by_key_nPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(sentry_value_u, ffi.Pointer, ffi.Size, - sentry_value_u)>>('sentry_value_set_by_key_n'); - late final _value_set_by_key_n = _value_set_by_key_nPtr.asFunction< - int Function( - sentry_value_u, ffi.Pointer, int, sentry_value_u)>(); - /// This removes a value from the map by key. int value_remove_by_key( sentry_value_u value, @@ -313,25 +184,6 @@ class SentryNative { late final _value_remove_by_key = _value_remove_by_keyPtr .asFunction)>(); - int value_remove_by_key_n( - sentry_value_u value, - ffi.Pointer k, - int k_len, - ) { - return _value_remove_by_key_n( - value, - k, - k_len, - ); - } - - late final _value_remove_by_key_nPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(sentry_value_u, ffi.Pointer, - ffi.Size)>>('sentry_value_remove_by_key_n'); - late final _value_remove_by_key_n = _value_remove_by_key_nPtr - .asFunction, int)>(); - /// Appends a value to a list. /// /// This moves the ownership of the value into the list. The caller does not @@ -352,49 +204,6 @@ class SentryNative { late final _value_append = _value_appendPtr .asFunction(); - /// Inserts a value into the list at a certain position. - /// - /// This moves the ownership of the value into the list. The caller does not - /// have to call `sentry_value_decref` on it. - /// - /// If the list is shorter than the given index it's automatically extended - /// and filled with `null` values. - int value_set_by_index( - sentry_value_u value, - int index, - sentry_value_u v, - ) { - return _value_set_by_index( - value, - index, - v, - ); - } - - late final _value_set_by_indexPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(sentry_value_u, ffi.Size, - sentry_value_u)>>('sentry_value_set_by_index'); - late final _value_set_by_index = _value_set_by_indexPtr - .asFunction(); - - /// This removes a value from the list by index. - int value_remove_by_index( - sentry_value_u value, - int index, - ) { - return _value_remove_by_index( - value, - index, - ); - } - - late final _value_remove_by_indexPtr = - _lookup>( - 'sentry_value_remove_by_index'); - late final _value_remove_by_index = - _value_remove_by_indexPtr.asFunction(); - /// Looks up a value in a map by key. If missing a null value is returned. /// The returned value is borrowed. sentry_value_u value_get_by_key( @@ -414,68 +223,6 @@ class SentryNative { late final _value_get_by_key = _value_get_by_keyPtr.asFunction< sentry_value_u Function(sentry_value_u, ffi.Pointer)>(); - sentry_value_u value_get_by_key_n( - sentry_value_u value, - ffi.Pointer k, - int k_len, - ) { - return _value_get_by_key_n( - value, - k, - k_len, - ); - } - - late final _value_get_by_key_nPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer, - ffi.Size)>>('sentry_value_get_by_key_n'); - late final _value_get_by_key_n = _value_get_by_key_nPtr.asFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer, int)>(); - - /// Looks up a value in a map by key. If missing a null value is returned. - /// The returned value is owned. - /// - /// If the caller no longer needs the value it must be released with - /// `sentry_value_decref`. - sentry_value_u value_get_by_key_owned( - sentry_value_u value, - ffi.Pointer k, - ) { - return _value_get_by_key_owned( - value, - k, - ); - } - - late final _value_get_by_key_ownedPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, - ffi.Pointer)>>('sentry_value_get_by_key_owned'); - late final _value_get_by_key_owned = _value_get_by_key_ownedPtr.asFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer)>(); - - sentry_value_u value_get_by_key_owned_n( - sentry_value_u value, - ffi.Pointer k, - int k_len, - ) { - return _value_get_by_key_owned_n( - value, - k, - k_len, - ); - } - - late final _value_get_by_key_owned_nPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer, - ffi.Size)>>('sentry_value_get_by_key_owned_n'); - late final _value_get_by_key_owned_n = - _value_get_by_key_owned_nPtr.asFunction< - sentry_value_u Function( - sentry_value_u, ffi.Pointer, int)>(); - /// Looks up a value in a list by index. If missing a null value is returned. /// The returned value is borrowed. sentry_value_u value_get_by_index( @@ -495,28 +242,6 @@ class SentryNative { late final _value_get_by_index = _value_get_by_indexPtr .asFunction(); - /// Looks up a value in a list by index. If missing a null value is - /// returned. The returned value is owned. - /// - /// If the caller no longer needs the value it must be released with - /// `sentry_value_decref`. - sentry_value_u value_get_by_index_owned( - sentry_value_u value, - int index, - ) { - return _value_get_by_index_owned( - value, - index, - ); - } - - late final _value_get_by_index_ownedPtr = _lookup< - ffi - .NativeFunction>( - 'sentry_value_get_by_index_owned'); - late final _value_get_by_index_owned = _value_get_by_index_ownedPtr - .asFunction(); - /// Returns the length of the given map or list. /// /// If an item is not a list or map the return value is 0. @@ -609,4048 +334,469 @@ class SentryNative { late final _value_is_null = _value_is_nullPtr.asFunction(); - /// Serialize a sentry value to JSON. - /// - /// The string is freshly allocated and must be freed with - /// `sentry_string_free`. - ffi.Pointer value_to_json( - sentry_value_u value, - ) { - return _value_to_json( - value, - ); - } - - late final _value_to_jsonPtr = _lookup< - ffi.NativeFunction Function(sentry_value_u)>>( - 'sentry_value_to_json'); - late final _value_to_json = _value_to_jsonPtr - .asFunction Function(sentry_value_u)>(); - - /// Creates a new empty Event value. - /// - /// See https://docs.sentry.io/platforms/native/enriching-events/ for how to - /// further work with events, and https://develop.sentry.dev/sdk/event-payloads/ - /// for a detailed overview of the possible properties of an Event. - sentry_value_u value_new_event() { - return _value_new_event(); + /// Creates a new options struct. + /// Can be freed with `sentry_options_free`. + ffi.Pointer options_new() { + return _options_new(); } - late final _value_new_eventPtr = - _lookup>( - 'sentry_value_new_event'); - late final _value_new_event = - _value_new_eventPtr.asFunction(); + late final _options_newPtr = + _lookup Function()>>( + 'sentry_options_new'); + late final _options_new = + _options_newPtr.asFunction Function()>(); - /// Creates a new Message Event value. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/message/ - /// - /// `logger` can be NULL to omit the logger value. - sentry_value_u value_new_message_event( - int level, - ffi.Pointer logger, - ffi.Pointer text, + /// Deallocates previously allocated sentry options. + void options_free( + ffi.Pointer opts, ) { - return _value_new_message_event( - level, - logger, - text, + return _options_free( + opts, ); } - late final _value_new_message_eventPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(ffi.Int32, ffi.Pointer, - ffi.Pointer)>>('sentry_value_new_message_event'); - late final _value_new_message_event = _value_new_message_eventPtr.asFunction< - sentry_value_u Function( - int, ffi.Pointer, ffi.Pointer)>(); + late final _options_freePtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_free'); + late final _options_free = _options_freePtr + .asFunction)>(); - sentry_value_u value_new_message_event_n( - int level, - ffi.Pointer logger, - int logger_len, - ffi.Pointer text, - int text_len, + /// Sets the DSN. + void options_set_dsn( + ffi.Pointer opts, + ffi.Pointer dsn, ) { - return _value_new_message_event_n( - level, - logger, - logger_len, - text, - text_len, + return _options_set_dsn( + opts, + dsn, ); } - late final _value_new_message_event_nPtr = _lookup< + late final _options_set_dsnPtr = _lookup< ffi.NativeFunction< - sentry_value_u Function( - ffi.Int32, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_value_new_message_event_n'); - late final _value_new_message_event_n = - _value_new_message_event_nPtr.asFunction< - sentry_value_u Function( - int, ffi.Pointer, int, ffi.Pointer, int)>(); + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dsn'); + late final _options_set_dsn = _options_set_dsnPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); - /// Creates a new Breadcrumb with a specific type and message. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/ - /// - /// Either parameter can be NULL in which case no such attributes is created. - sentry_value_u value_new_breadcrumb( - ffi.Pointer type, - ffi.Pointer message, + /// Gets the DSN. + ffi.Pointer options_get_dsn( + ffi.Pointer opts, ) { - return _value_new_breadcrumb( - type, - message, + return _options_get_dsn( + opts, ); } - late final _value_new_breadcrumbPtr = _lookup< + late final _options_get_dsnPtr = _lookup< ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer, - ffi.Pointer)>>('sentry_value_new_breadcrumb'); - late final _value_new_breadcrumb = _value_new_breadcrumbPtr.asFunction< - sentry_value_u Function(ffi.Pointer, ffi.Pointer)>(); + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dsn'); + late final _options_get_dsn = _options_get_dsnPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); - sentry_value_u value_new_breadcrumb_n( - ffi.Pointer type, - int type_len, - ffi.Pointer message, - int message_len, + /// Sets the release. + void options_set_release( + ffi.Pointer opts, + ffi.Pointer release, ) { - return _value_new_breadcrumb_n( - type, - type_len, - message, - message_len, + return _options_set_release( + opts, + release, ); } - late final _value_new_breadcrumb_nPtr = _lookup< + late final _options_set_releasePtr = _lookup< ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_value_new_breadcrumb_n'); - late final _value_new_breadcrumb_n = _value_new_breadcrumb_nPtr.asFunction< - sentry_value_u Function( - ffi.Pointer, int, ffi.Pointer, int)>(); + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_release'); + late final _options_set_release = _options_set_releasePtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); - /// Creates a new Exception value. - /// - /// This is intended for capturing language-level exception, such as from a - /// try-catch block. `type` and `value` here refer to the exception class and - /// a possible description. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/exception/ - /// - /// The returned value needs to be attached to an event via - /// `sentry_event_add_exception`. - sentry_value_u value_new_exception( - ffi.Pointer type, - ffi.Pointer value, + /// Gets the release. + ffi.Pointer options_get_release( + ffi.Pointer opts, ) { - return _value_new_exception( - type, - value, + return _options_get_release( + opts, ); } - late final _value_new_exceptionPtr = _lookup< + late final _options_get_releasePtr = _lookup< ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer, - ffi.Pointer)>>('sentry_value_new_exception'); - late final _value_new_exception = _value_new_exceptionPtr.asFunction< - sentry_value_u Function(ffi.Pointer, ffi.Pointer)>(); + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_release'); + late final _options_get_release = _options_get_releasePtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); - sentry_value_u value_new_exception_n( - ffi.Pointer type, - int type_len, - ffi.Pointer value, - int value_len, + /// Sets the environment. + void options_set_environment( + ffi.Pointer opts, + ffi.Pointer environment, ) { - return _value_new_exception_n( - type, - type_len, - value, - value_len, + return _options_set_environment( + opts, + environment, ); } - late final _value_new_exception_nPtr = _lookup< + late final _options_set_environmentPtr = _lookup< ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_value_new_exception_n'); - late final _value_new_exception_n = _value_new_exception_nPtr.asFunction< - sentry_value_u Function( - ffi.Pointer, int, ffi.Pointer, int)>(); + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_environment'); + late final _options_set_environment = _options_set_environmentPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); - /// Creates a new Thread value. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/threads/ - /// - /// The returned value needs to be attached to an event via - /// `sentry_event_add_thread`. - /// - /// `name` can be NULL. - sentry_value_u value_new_thread( - int id, - ffi.Pointer name, + /// Gets the environment. + ffi.Pointer options_get_environment( + ffi.Pointer opts, ) { - return _value_new_thread( - id, - name, + return _options_get_environment( + opts, ); } - late final _value_new_threadPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function( - ffi.Uint64, ffi.Pointer)>>('sentry_value_new_thread'); - late final _value_new_thread = _value_new_threadPtr - .asFunction)>(); - - sentry_value_u value_new_thread_n( - int id, - ffi.Pointer name, - int name_len, - ) { - return _value_new_thread_n( - id, - name, - name_len, - ); - } - - late final _value_new_thread_nPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(ffi.Uint64, ffi.Pointer, - ffi.Size)>>('sentry_value_new_thread_n'); - late final _value_new_thread_n = _value_new_thread_nPtr - .asFunction, int)>(); - - /// Creates a new Stack Trace conforming to the Stack Trace Interface. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/stacktrace/ - /// - /// The returned object must be attached to either an exception or thread - /// object. - /// - /// If `ips` is NULL the current stack trace is captured, otherwise `len` - /// stack trace instruction pointers are attached to the event. - sentry_value_u value_new_stacktrace( - ffi.Pointer> ips, - int len, - ) { - return _value_new_stacktrace( - ips, - len, - ); - } - - late final _value_new_stacktracePtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer>, - ffi.Size)>>('sentry_value_new_stacktrace'); - late final _value_new_stacktrace = _value_new_stacktracePtr.asFunction< - sentry_value_u Function(ffi.Pointer>, int)>(); - - /// Sets the Stack Trace conforming to the Stack Trace Interface in a value. - /// - /// The value argument must be either an exception or thread object. - /// - /// If `ips` is NULL the current stack trace is captured, otherwise `len` stack - /// trace instruction pointers are attached to the event. - void value_set_stacktrace( - sentry_value_u value, - ffi.Pointer> ips, - int len, - ) { - return _value_set_stacktrace( - value, - ips, - len, - ); - } - - late final _value_set_stacktracePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(sentry_value_u, ffi.Pointer>, - ffi.Size)>>('sentry_value_set_stacktrace'); - late final _value_set_stacktrace = _value_set_stacktracePtr.asFunction< - void Function(sentry_value_u, ffi.Pointer>, int)>(); - - /// Adds an Exception to an Event value. - /// - /// This takes ownership of the `exception`. - void event_add_exception( - sentry_value_u event, - sentry_value_u exception, - ) { - return _event_add_exception( - event, - exception, - ); - } - - late final _event_add_exceptionPtr = _lookup< - ffi - .NativeFunction>( - 'sentry_event_add_exception'); - late final _event_add_exception = _event_add_exceptionPtr - .asFunction(); - - /// Adds a Thread to an Event value. - /// - /// This takes ownership of the `thread`. - void event_add_thread( - sentry_value_u event, - sentry_value_u thread, - ) { - return _event_add_thread( - event, - thread, - ); - } - - late final _event_add_threadPtr = _lookup< - ffi - .NativeFunction>( - 'sentry_event_add_thread'); - late final _event_add_thread = _event_add_threadPtr - .asFunction(); - - /// Serialize a sentry value to msgpack. - /// - /// The string is freshly allocated and must be freed with - /// `sentry_string_free`. Since msgpack is not zero terminated - /// the size is written to the `size_out` parameter. - ffi.Pointer value_to_msgpack( - sentry_value_u value, - ffi.Pointer size_out, - ) { - return _value_to_msgpack( - value, - size_out, - ); - } - - late final _value_to_msgpackPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(sentry_value_u, - ffi.Pointer)>>('sentry_value_to_msgpack'); - late final _value_to_msgpack = _value_to_msgpackPtr.asFunction< - ffi.Pointer Function(sentry_value_u, ffi.Pointer)>(); - - /// Adds a stack trace to an event. - /// - /// The stack trace is added as part of a new thread object. - /// This function is **deprecated** in favor of using - /// `sentry_value_new_stacktrace` in combination with `sentry_value_new_thread` - /// and `sentry_event_add_thread`. - /// - /// If `ips` is NULL the current stack trace is captured, otherwise `len` - /// stack trace instruction pointers are attached to the event. - void event_value_add_stacktrace( - sentry_value_u event, - ffi.Pointer> ips, - int len, - ) { - return _event_value_add_stacktrace( - event, - ips, - len, - ); - } - - late final _event_value_add_stacktracePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(sentry_value_u, ffi.Pointer>, - ffi.Size)>>('sentry_event_value_add_stacktrace'); - late final _event_value_add_stacktrace = - _event_value_add_stacktracePtr.asFunction< - void Function( - sentry_value_u, ffi.Pointer>, int)>(); - - /// Unwinds the stack from the given address. - /// - /// If the address is given in `addr` the stack is unwound form there. - /// Otherwise (NULL is passed) the current instruction pointer is used as - /// start address. - /// Unwinding with a given `addr` is not supported on all platforms. - /// - /// The stack trace in the form of instruction-addresses, is written to the - /// caller allocated `stacktrace_out`, with up to `max_len` frames being written. - /// The actual number of unwound stackframes is returned. - int unwind_stack( - ffi.Pointer addr, - ffi.Pointer> stacktrace_out, - int max_len, - ) { - return _unwind_stack( - addr, - stacktrace_out, - max_len, - ); - } - - late final _unwind_stackPtr = _lookup< - ffi.NativeFunction< - ffi.Size Function( - ffi.Pointer, - ffi.Pointer>, - ffi.Size)>>('sentry_unwind_stack'); - late final _unwind_stack = _unwind_stackPtr.asFunction< - int Function( - ffi.Pointer, ffi.Pointer>, int)>(); - - /// Unwinds the stack from the given context. - /// - /// The caller is responsible to construct an appropriate `sentry_ucontext_t`. - /// Unwinding from a user context is not supported on all platforms. - /// - /// The stack trace in the form of instruction-addresses, is written to the - /// caller allocated `stacktrace_out`, with up to `max_len` frames being written. - /// The actual number of unwound stackframes is returned. - int unwind_stack_from_ucontext( - ffi.Pointer uctx, - ffi.Pointer> stacktrace_out, - int max_len, - ) { - return _unwind_stack_from_ucontext( - uctx, - stacktrace_out, - max_len, - ); - } - - late final _unwind_stack_from_ucontextPtr = _lookup< - ffi.NativeFunction< - ffi.Size Function( - ffi.Pointer, - ffi.Pointer>, - ffi.Size)>>('sentry_unwind_stack_from_ucontext'); - late final _unwind_stack_from_ucontext = - _unwind_stack_from_ucontextPtr.asFunction< - int Function(ffi.Pointer, - ffi.Pointer>, int)>(); - - /// Creates the nil uuid. - sentry_uuid_s uuid_nil() { - return _uuid_nil(); - } - - late final _uuid_nilPtr = - _lookup>('sentry_uuid_nil'); - late final _uuid_nil = _uuid_nilPtr.asFunction(); - - /// Creates a new uuid4. - sentry_uuid_s uuid_new_v4() { - return _uuid_new_v4(); - } - - late final _uuid_new_v4Ptr = - _lookup>( - 'sentry_uuid_new_v4'); - late final _uuid_new_v4 = - _uuid_new_v4Ptr.asFunction(); - - /// Parses a uuid from a string. - sentry_uuid_s uuid_from_string( - ffi.Pointer str, - ) { - return _uuid_from_string( - str, - ); - } - - late final _uuid_from_stringPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_uuid_from_string'); - late final _uuid_from_string = _uuid_from_stringPtr - .asFunction)>(); - - sentry_uuid_s uuid_from_string_n( - ffi.Pointer str, - int str_len, - ) { - return _uuid_from_string_n( - str, - str_len, - ); - } - - late final _uuid_from_string_nPtr = _lookup< - ffi.NativeFunction< - sentry_uuid_s Function( - ffi.Pointer, ffi.Size)>>('sentry_uuid_from_string_n'); - late final _uuid_from_string_n = _uuid_from_string_nPtr - .asFunction, int)>(); - - /// Creates a uuid from bytes. - sentry_uuid_s uuid_from_bytes( - ffi.Pointer bytes, - ) { - return _uuid_from_bytes( - bytes, - ); - } - - late final _uuid_from_bytesPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_uuid_from_bytes'); - late final _uuid_from_bytes = _uuid_from_bytesPtr - .asFunction)>(); - - /// Checks if the uuid is nil. - int uuid_is_nil( - ffi.Pointer uuid, - ) { - return _uuid_is_nil( - uuid, - ); - } - - late final _uuid_is_nilPtr = - _lookup)>>( - 'sentry_uuid_is_nil'); - late final _uuid_is_nil = - _uuid_is_nilPtr.asFunction)>(); - - /// Returns the bytes of the uuid. - void uuid_as_bytes( - ffi.Pointer uuid, - ffi.Pointer bytes, - ) { - return _uuid_as_bytes( - uuid, - bytes, - ); - } - - late final _uuid_as_bytesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_uuid_as_bytes'); - late final _uuid_as_bytes = _uuid_as_bytesPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - /// Formats the uuid into a string buffer. - void uuid_as_string( - ffi.Pointer uuid, - ffi.Pointer str, - ) { - return _uuid_as_string( - uuid, - str, - ); - } - - late final _uuid_as_stringPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_uuid_as_string'); - late final _uuid_as_string = _uuid_as_stringPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - /// Frees an envelope. - void envelope_free( - ffi.Pointer envelope, - ) { - return _envelope_free( - envelope, - ); - } - - late final _envelope_freePtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_envelope_free'); - late final _envelope_free = _envelope_freePtr - .asFunction)>(); - - /// Given an Envelope, returns the embedded Event if there is one. - /// - /// This returns a borrowed value to the Event in the Envelope. - sentry_value_u envelope_get_event( - ffi.Pointer envelope, - ) { - return _envelope_get_event( - envelope, - ); - } - - late final _envelope_get_eventPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer)>>('sentry_envelope_get_event'); - late final _envelope_get_event = _envelope_get_eventPtr - .asFunction)>(); - - /// Given an Envelope, returns the embedded Transaction if there is one. - /// - /// This returns a borrowed value to the Transaction in the Envelope. - sentry_value_u envelope_get_transaction( - ffi.Pointer envelope, - ) { - return _envelope_get_transaction( - envelope, - ); - } - - late final _envelope_get_transactionPtr = _lookup< + late final _options_get_environmentPtr = _lookup< ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer)>>( - 'sentry_envelope_get_transaction'); - late final _envelope_get_transaction = _envelope_get_transactionPtr - .asFunction)>(); - - /// Serializes the envelope. - /// - /// The return value needs to be freed with sentry_string_free(). - ffi.Pointer envelope_serialize( - ffi.Pointer envelope, - ffi.Pointer size_out, - ) { - return _envelope_serialize( - envelope, - size_out, - ); - } - - late final _envelope_serializePtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer)>>('sentry_envelope_serialize'); - late final _envelope_serialize = _envelope_serializePtr.asFunction< - ffi.Pointer Function( - ffi.Pointer, ffi.Pointer)>(); + ffi.Pointer Function(ffi.Pointer)>>( + 'sentry_options_get_environment'); + late final _options_get_environment = _options_get_environmentPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); - /// Serializes the envelope into a file. - /// - /// `path` is assumed to be in platform-specific filesystem path encoding. - /// - /// Returns 0 on success. - int envelope_write_to_file( - ffi.Pointer envelope, - ffi.Pointer path, + /// Sets the dist. + void options_set_dist( + ffi.Pointer opts, + ffi.Pointer dist, ) { - return _envelope_write_to_file( - envelope, - path, + return _options_set_dist( + opts, + dist, ); } - late final _envelope_write_to_filePtr = _lookup< + late final _options_set_distPtr = _lookup< ffi.NativeFunction< - ffi.Int Function(ffi.Pointer, - ffi.Pointer)>>('sentry_envelope_write_to_file'); - late final _envelope_write_to_file = _envelope_write_to_filePtr.asFunction< - int Function(ffi.Pointer, ffi.Pointer)>(); + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_dist'); + late final _options_set_dist = _options_set_distPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); - int envelope_write_to_file_n( - ffi.Pointer envelope, - ffi.Pointer path, - int path_len, + /// Gets the dist. + ffi.Pointer options_get_dist( + ffi.Pointer opts, ) { - return _envelope_write_to_file_n( - envelope, - path, - path_len, + return _options_get_dist( + opts, ); } - late final _envelope_write_to_file_nPtr = _lookup< + late final _options_get_distPtr = _lookup< ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_envelope_write_to_file_n'); - late final _envelope_write_to_file_n = - _envelope_write_to_file_nPtr.asFunction< - int Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Creates a new transport with an initial `send_func`. - ffi.Pointer transport_new( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer envelope, - ffi.Pointer state)>> - send_func, - ) { - return _transport_new( - send_func, - ); - } - - late final _transport_newPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer envelope, - ffi.Pointer state)>>)>>( - 'sentry_transport_new'); - late final _transport_new = _transport_newPtr.asFunction< - ffi.Pointer Function( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer envelope, - ffi.Pointer state)>>)>(); + ffi.Pointer Function( + ffi.Pointer)>>('sentry_options_get_dist'); + late final _options_get_dist = _options_get_distPtr.asFunction< + ffi.Pointer Function(ffi.Pointer)>(); - /// Sets the transport `state`. - /// - /// If the state is owned by the transport and needs to be freed, use - /// `sentry_transport_set_free_func` to set an appropriate hook. - void transport_set_state( - ffi.Pointer transport, - ffi.Pointer state, + /// Enables or disables debug printing mode. + void options_set_debug( + ffi.Pointer opts, + int debug, ) { - return _transport_set_state( - transport, - state, + return _options_set_debug( + opts, + debug, ); } - late final _transport_set_statePtr = _lookup< + late final _options_set_debugPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_transport_set_state'); - late final _transport_set_state = _transport_set_statePtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - /// Sets the transport hook to free the transport `state`. - void transport_set_free_func( - ffi.Pointer transport, - ffi.Pointer< - ffi.NativeFunction state)>> - free_func, - ) { - return _transport_set_free_func( - transport, - free_func, - ); - } - - late final _transport_set_free_funcPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer state)>>)>>( - 'sentry_transport_set_free_func'); - late final _transport_set_free_func = _transport_set_free_funcPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer state)>>)>(); - - /// Sets the transport startup hook. - /// - /// This hook is called from within `sentry_init` and will get a reference to the - /// options which can be used to initialize a transports internal state. - /// It should return `0` on success. A failure will bubble up to `sentry_init`. - void transport_set_startup_func( - ffi.Pointer transport, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer options, - ffi.Pointer state)>> - startup_func, - ) { - return _transport_set_startup_func( - transport, - startup_func, - ); - } - - late final _transport_set_startup_funcPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer options, - ffi.Pointer state)>>)>>( - 'sentry_transport_set_startup_func'); - late final _transport_set_startup_func = - _transport_set_startup_funcPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer options, - ffi.Pointer state)>>)>(); - - /// Sets the transport flush hook. - /// - /// This hook will receive a millisecond-resolution timeout. - /// It should return `0` if all the pending envelopes are - /// sent within the timeout, or `1` if the timeout is hit. - void transport_set_flush_func( - ffi.Pointer transport, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Uint64 timeout, ffi.Pointer state)>> - flush_func, - ) { - return _transport_set_flush_func( - transport, - flush_func, - ); - } - - late final _transport_set_flush_funcPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Uint64 timeout, - ffi.Pointer state)>>)>>( - 'sentry_transport_set_flush_func'); - late final _transport_set_flush_func = - _transport_set_flush_funcPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Uint64 timeout, - ffi.Pointer state)>>)>(); - - /// Sets the transport shutdown hook. - /// - /// This hook will receive a millisecond-resolution timeout. - /// It should return `0` on success in case all the pending envelopes have been - /// sent within the timeout, or `1` if the timeout was hit. - void transport_set_shutdown_func( - ffi.Pointer transport, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Uint64 timeout, ffi.Pointer state)>> - shutdown_func, - ) { - return _transport_set_shutdown_func( - transport, - shutdown_func, - ); - } - - late final _transport_set_shutdown_funcPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Uint64 timeout, - ffi.Pointer state)>>)>>( - 'sentry_transport_set_shutdown_func'); - late final _transport_set_shutdown_func = - _transport_set_shutdown_funcPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Uint64 timeout, - ffi.Pointer state)>>)>(); + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_debug'); + late final _options_set_debug = _options_set_debugPtr + .asFunction, int)>(); - /// Generic way to free a transport. - void transport_free( - ffi.Pointer transport, + /// Returns the current value of the debug flag. + int options_get_debug( + ffi.Pointer opts, ) { - return _transport_free( - transport, + return _options_get_debug( + opts, ); } - late final _transport_freePtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_transport_free'); - late final _transport_free = _transport_freePtr - .asFunction)>(); + late final _options_get_debugPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_debug'); + late final _options_get_debug = _options_get_debugPtr + .asFunction)>(); - /// Create a new function transport. - /// - /// It is a convenience function which works with a borrowed `data`, and will - /// automatically free the envelope, so the user provided function does not need - /// to do that. + /// Sets the number of breadcrumbs being tracked and attached to events. /// - /// This function is *deprecated* and will be removed in a future version. - /// It is here for backwards compatibility. Users should migrate to the - /// `sentry_transport_new` API. - ffi.Pointer new_function_transport( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer envelope, - ffi.Pointer data)>> - func, - ffi.Pointer data, - ) { - return _new_function_transport( - func, - data, - ); - } - - late final _new_function_transportPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer envelope, - ffi.Pointer data)>>, - ffi.Pointer)>>('sentry_new_function_transport'); - late final _new_function_transport = _new_function_transportPtr.asFunction< - ffi.Pointer Function( - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer envelope, - ffi.Pointer data)>>, - ffi.Pointer)>(); - - /// Creates a new options struct. - /// Can be freed with `sentry_options_free`. - ffi.Pointer options_new() { - return _options_new(); - } - - late final _options_newPtr = - _lookup Function()>>( - 'sentry_options_new'); - late final _options_new = - _options_newPtr.asFunction Function()>(); - - /// Deallocates previously allocated sentry options. - void options_free( + /// Defaults to 100. + void options_set_max_breadcrumbs( ffi.Pointer opts, + int max_breadcrumbs, ) { - return _options_free( + return _options_set_max_breadcrumbs( opts, + max_breadcrumbs, ); } - late final _options_freePtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_free'); - late final _options_free = _options_freePtr - .asFunction)>(); - - /// Sets a transport. - void options_set_transport( - ffi.Pointer opts, - ffi.Pointer transport, - ) { - return _options_set_transport( - opts, - transport, - ); - } - - late final _options_set_transportPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>( - 'sentry_options_set_transport'); - late final _options_set_transport = _options_set_transportPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - /// Sets the `before_send` callback. - /// - /// See the `sentry_event_function_t` typedef above for more information. - void options_set_before_send( - ffi.Pointer opts, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer, - ffi.Pointer)>> - func, - ffi.Pointer data, - ) { - return _options_set_before_send( - opts, - func, - data, - ); - } - - late final _options_set_before_sendPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>>('sentry_options_set_before_send'); - late final _options_set_before_send = _options_set_before_sendPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(sentry_value_u, ffi.Pointer, - ffi.Pointer)>>, - ffi.Pointer)>(); - - /// Sets the `on_crash` callback. - /// - /// See the `sentry_crash_function_t` typedef above for more information. - void options_set_on_crash( - ffi.Pointer opts, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer, - sentry_value_u, ffi.Pointer)>> - func, - ffi.Pointer data, - ) { - return _options_set_on_crash( - opts, - func, - data, - ); - } - - late final _options_set_on_crashPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer, - sentry_value_u, ffi.Pointer)>>, - ffi.Pointer)>>('sentry_options_set_on_crash'); - late final _options_set_on_crash = _options_set_on_crashPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - sentry_value_u Function(ffi.Pointer, - sentry_value_u, ffi.Pointer)>>, - ffi.Pointer)>(); - - /// Sets the DSN. - void options_set_dsn( - ffi.Pointer opts, - ffi.Pointer dsn, - ) { - return _options_set_dsn( - opts, - dsn, - ); - } - - late final _options_set_dsnPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_dsn'); - late final _options_set_dsn = _options_set_dsnPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_dsn_n( - ffi.Pointer opts, - ffi.Pointer dsn, - int dsn_len, - ) { - return _options_set_dsn_n( - opts, - dsn, - dsn_len, - ); - } - - late final _options_set_dsn_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Size)>>('sentry_options_set_dsn_n'); - late final _options_set_dsn_n = _options_set_dsn_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Gets the DSN. - ffi.Pointer options_get_dsn( - ffi.Pointer opts, - ) { - return _options_get_dsn( - opts, - ); - } - - late final _options_get_dsnPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_dsn'); - late final _options_get_dsn = _options_get_dsnPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Sets the sample rate, which should be a double between `0.0` and `1.0`. - /// Sentry will randomly discard any event that is captured using - /// `sentry_capture_event` when a sample rate < 1 is set. - /// - /// The sampling happens at the end of the event processing according to the - /// following order: - /// - /// https://develop.sentry.dev/sdk/sessions/#filter-order - /// - /// Only items 3. to 6. are currently applicable to sentry-native. This means - /// each processing step is executed even if the sampling discards the event - /// before sending it to the backend. This is particularly relevant to users of - /// the `before_send` callback. - /// - /// The above is in contrast to versions up to 0.4.18 where the sampling happened - /// at the beginning of the processing/filter sequence. - void options_set_sample_rate( - ffi.Pointer opts, - double sample_rate, - ) { - return _options_set_sample_rate( - opts, - sample_rate, - ); - } - - late final _options_set_sample_ratePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Double)>>('sentry_options_set_sample_rate'); - late final _options_set_sample_rate = _options_set_sample_ratePtr - .asFunction, double)>(); - - /// Gets the sample rate. - double options_get_sample_rate( - ffi.Pointer opts, - ) { - return _options_get_sample_rate( - opts, - ); - } - - late final _options_get_sample_ratePtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_options_get_sample_rate'); - late final _options_get_sample_rate = _options_get_sample_ratePtr - .asFunction)>(); - - /// Sets the release. - void options_set_release( - ffi.Pointer opts, - ffi.Pointer release, - ) { - return _options_set_release( - opts, - release, - ); - } - - late final _options_set_releasePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_release'); - late final _options_set_release = _options_set_releasePtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_release_n( - ffi.Pointer opts, - ffi.Pointer release, - int release_len, - ) { - return _options_set_release_n( - opts, - release, - release_len, - ); - } - - late final _options_set_release_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_release_n'); - late final _options_set_release_n = _options_set_release_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Gets the release. - ffi.Pointer options_get_release( - ffi.Pointer opts, - ) { - return _options_get_release( - opts, - ); - } - - late final _options_get_releasePtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_release'); - late final _options_get_release = _options_get_releasePtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Sets the environment. - void options_set_environment( - ffi.Pointer opts, - ffi.Pointer environment, - ) { - return _options_set_environment( - opts, - environment, - ); - } - - late final _options_set_environmentPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_environment'); - late final _options_set_environment = _options_set_environmentPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_environment_n( - ffi.Pointer opts, - ffi.Pointer environment, - int environment_len, - ) { - return _options_set_environment_n( - opts, - environment, - environment_len, - ); - } - - late final _options_set_environment_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_environment_n'); - late final _options_set_environment_n = - _options_set_environment_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Gets the environment. - ffi.Pointer options_get_environment( - ffi.Pointer opts, - ) { - return _options_get_environment( - opts, - ); - } - - late final _options_get_environmentPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer)>>( - 'sentry_options_get_environment'); - late final _options_get_environment = _options_get_environmentPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Sets the dist. - void options_set_dist( - ffi.Pointer opts, - ffi.Pointer dist, - ) { - return _options_set_dist( - opts, - dist, - ); - } - - late final _options_set_distPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_dist'); - late final _options_set_dist = _options_set_distPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_dist_n( - ffi.Pointer opts, - ffi.Pointer dist, - int dist_len, - ) { - return _options_set_dist_n( - opts, - dist, - dist_len, - ); - } - - late final _options_set_dist_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Size)>>('sentry_options_set_dist_n'); - late final _options_set_dist_n = _options_set_dist_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Gets the dist. - ffi.Pointer options_get_dist( - ffi.Pointer opts, - ) { - return _options_get_dist( - opts, - ); - } - - late final _options_get_distPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_dist'); - late final _options_get_dist = _options_get_distPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Configures the http proxy. - /// - /// The given proxy has to include the full scheme, eg. `http://some.proxy/`. - void options_set_http_proxy( - ffi.Pointer opts, - ffi.Pointer proxy, - ) { - return _options_set_http_proxy( - opts, - proxy, - ); - } - - late final _options_set_http_proxyPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_http_proxy'); - late final _options_set_http_proxy = _options_set_http_proxyPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_http_proxy_n( - ffi.Pointer opts, - ffi.Pointer proxy, - int proxy_len, - ) { - return _options_set_http_proxy_n( - opts, - proxy, - proxy_len, - ); - } - - late final _options_set_http_proxy_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_http_proxy_n'); - late final _options_set_http_proxy_n = - _options_set_http_proxy_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Returns the configured http proxy. - ffi.Pointer options_get_http_proxy( - ffi.Pointer opts, - ) { - return _options_get_http_proxy( - opts, - ); - } - - late final _options_get_http_proxyPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_http_proxy'); - late final _options_get_http_proxy = _options_get_http_proxyPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Configures the path to a file containing ssl certificates for - /// verification. - void options_set_ca_certs( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_set_ca_certs( - opts, - path, - ); - } - - late final _options_set_ca_certsPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_ca_certs'); - late final _options_set_ca_certs = _options_set_ca_certsPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_set_ca_certs_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_set_ca_certs_n( - opts, - path, - path_len, - ); - } - - late final _options_set_ca_certs_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_ca_certs_n'); - late final _options_set_ca_certs_n = _options_set_ca_certs_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Returns the configured path for ca certificates. - ffi.Pointer options_get_ca_certs( - ffi.Pointer opts, - ) { - return _options_get_ca_certs( - opts, - ); - } - - late final _options_get_ca_certsPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_ca_certs'); - late final _options_get_ca_certs = _options_get_ca_certsPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Configures the name of the http transport thread. - void options_set_transport_thread_name( - ffi.Pointer opts, - ffi.Pointer name, - ) { - return _options_set_transport_thread_name( - opts, - name, - ); - } - - late final _options_set_transport_thread_namePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.Pointer)>>( - 'sentry_options_set_transport_thread_name'); - late final _options_set_transport_thread_name = - _options_set_transport_thread_namePtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void options_set_transport_thread_name_n( - ffi.Pointer opts, - ffi.Pointer name, - int name_len, - ) { - return _options_set_transport_thread_name_n( - opts, - name, - name_len, - ); - } - - late final _options_set_transport_thread_name_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_transport_thread_name_n'); - late final _options_set_transport_thread_name_n = - _options_set_transport_thread_name_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Returns the configured http transport thread name. - ffi.Pointer options_get_transport_thread_name( - ffi.Pointer opts, - ) { - return _options_get_transport_thread_name( - opts, - ); - } - - late final _options_get_transport_thread_namePtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function(ffi.Pointer)>>( - 'sentry_options_get_transport_thread_name'); - late final _options_get_transport_thread_name = - _options_get_transport_thread_namePtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Configures the name of the sentry SDK. Returns 0 on success. - int options_set_sdk_name( - ffi.Pointer opts, - ffi.Pointer sdk_name, - ) { - return _options_set_sdk_name( - opts, - sdk_name, - ); - } - - late final _options_set_sdk_namePtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_sdk_name'); - late final _options_set_sdk_name = _options_set_sdk_namePtr.asFunction< - int Function(ffi.Pointer, ffi.Pointer)>(); - - /// Configures the name of the sentry SDK. Returns 0 on success. - int options_set_sdk_name_n( - ffi.Pointer opts, - ffi.Pointer sdk_name, - int sdk_name_len, - ) { - return _options_set_sdk_name_n( - opts, - sdk_name, - sdk_name_len, - ); - } - - late final _options_set_sdk_name_nPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer, ffi.Pointer, - ffi.Size)>>('sentry_options_set_sdk_name_n'); - late final _options_set_sdk_name_n = _options_set_sdk_name_nPtr.asFunction< - int Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Returns the configured sentry SDK name. Unless overwritten this defaults to - /// SENTRY_SDK_NAME. - ffi.Pointer options_get_sdk_name( - ffi.Pointer opts, - ) { - return _options_get_sdk_name( - opts, - ); - } - - late final _options_get_sdk_namePtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_sdk_name'); - late final _options_get_sdk_name = _options_get_sdk_namePtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Returns the user agent. Unless overwritten this defaults to - /// "SENTRY_SDK_NAME / SENTRY_SDK_VERSION". - ffi.Pointer options_get_user_agent( - ffi.Pointer opts, - ) { - return _options_get_user_agent( - opts, - ); - } - - late final _options_get_user_agentPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer)>>('sentry_options_get_user_agent'); - late final _options_get_user_agent = _options_get_user_agentPtr.asFunction< - ffi.Pointer Function(ffi.Pointer)>(); - - /// Enables or disables debug printing mode. - void options_set_debug( - ffi.Pointer opts, - int debug, - ) { - return _options_set_debug( - opts, - debug, - ); - } - - late final _options_set_debugPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_debug'); - late final _options_set_debug = _options_set_debugPtr - .asFunction, int)>(); - - /// Returns the current value of the debug flag. - int options_get_debug( - ffi.Pointer opts, - ) { - return _options_get_debug( - opts, - ); - } - - late final _options_get_debugPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_debug'); - late final _options_get_debug = _options_get_debugPtr - .asFunction)>(); - - /// Sets the number of breadcrumbs being tracked and attached to events. - /// - /// Defaults to 100. - void options_set_max_breadcrumbs( - ffi.Pointer opts, - int max_breadcrumbs, - ) { - return _options_set_max_breadcrumbs( - opts, - max_breadcrumbs, - ); - } - - late final _options_set_max_breadcrumbsPtr = _lookup< + late final _options_set_max_breadcrumbsPtr = _lookup< ffi.NativeFunction< ffi.Void Function(ffi.Pointer, ffi.Size)>>('sentry_options_set_max_breadcrumbs'); late final _options_set_max_breadcrumbs = _options_set_max_breadcrumbsPtr .asFunction, int)>(); - /// Gets the number of breadcrumbs being tracked and attached to events. - int options_get_max_breadcrumbs( - ffi.Pointer opts, - ) { - return _options_get_max_breadcrumbs( - opts, - ); - } - - late final _options_get_max_breadcrumbsPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_max_breadcrumbs'); - late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr - .asFunction)>(); - - /// 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. - void options_set_logger( - ffi.Pointer opts, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Int32, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>> - func, - ffi.Pointer userdata, - ) { - return _options_set_logger( - opts, - func, - userdata, - ); - } - - late final _options_set_loggerPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Int32, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>>('sentry_options_set_logger'); - late final _options_set_logger = _options_set_loggerPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Int32, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>(); - - /// Enables or disables automatic session tracking. - /// - /// Automatic session tracking is enabled by default and is equivalent to calling - /// `sentry_start_session` after startup. - /// There can only be one running session, and the current session will always be - /// closed implicitly by `sentry_close`, when starting a new session with - /// `sentry_start_session`, or manually by calling `sentry_end_session`. - void options_set_auto_session_tracking( - ffi.Pointer opts, - int val, - ) { - return _options_set_auto_session_tracking( - opts, - val, - ); - } - - late final _options_set_auto_session_trackingPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_auto_session_tracking'); - late final _options_set_auto_session_tracking = - _options_set_auto_session_trackingPtr - .asFunction, int)>(); - - /// Returns true if automatic session tracking is enabled. - int options_get_auto_session_tracking( - ffi.Pointer opts, - ) { - return _options_get_auto_session_tracking( - opts, - ); - } - - late final _options_get_auto_session_trackingPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_auto_session_tracking'); - late final _options_get_auto_session_tracking = - _options_get_auto_session_trackingPtr - .asFunction)>(); - - /// Enables or disables user consent requirements for uploads. - /// - /// This disables uploads until the user has given the consent to the SDK. - /// Consent itself is given with `sentry_user_consent_give` and - /// `sentry_user_consent_revoke`. - void options_set_require_user_consent( - ffi.Pointer opts, - int val, - ) { - return _options_set_require_user_consent( - opts, - val, - ); - } - - late final _options_set_require_user_consentPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_require_user_consent'); - late final _options_set_require_user_consent = - _options_set_require_user_consentPtr - .asFunction, int)>(); - - /// Returns true if user consent is required. - int options_get_require_user_consent( - ffi.Pointer opts, - ) { - return _options_get_require_user_consent( - opts, - ); - } - - late final _options_get_require_user_consentPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_require_user_consent'); - late final _options_get_require_user_consent = - _options_get_require_user_consentPtr - .asFunction)>(); - - /// Enables or disables on-device symbolication of stack traces. - /// - /// This feature can have a performance impact, and is enabled by default on - /// Android. It is usually only needed when it is not possible to provide debug - /// information files for system libraries which are needed for serverside - /// symbolication. - void options_set_symbolize_stacktraces( - ffi.Pointer opts, - int val, - ) { - return _options_set_symbolize_stacktraces( - opts, - val, - ); - } - - late final _options_set_symbolize_stacktracesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_symbolize_stacktraces'); - late final _options_set_symbolize_stacktraces = - _options_set_symbolize_stacktracesPtr - .asFunction, int)>(); - - /// Returns true if on-device symbolication of stack traces is enabled. - int options_get_symbolize_stacktraces( - ffi.Pointer opts, - ) { - return _options_get_symbolize_stacktraces( - opts, - ); - } - - late final _options_get_symbolize_stacktracesPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_symbolize_stacktraces'); - late final _options_get_symbolize_stacktraces = - _options_get_symbolize_stacktracesPtr - .asFunction)>(); - - /// Adds a new attachment to be sent along. - /// - /// `path` is assumed to be in platform-specific filesystem path encoding. - /// API Users on windows are encouraged to use `sentry_options_add_attachmentw` - /// instead. - void options_add_attachment( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_add_attachment( - opts, - path, - ); - } - - late final _options_add_attachmentPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_add_attachment'); - late final _options_add_attachment = _options_add_attachmentPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_add_attachment_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_add_attachment_n( - opts, - path, - path_len, - ); - } - - late final _options_add_attachment_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_add_attachment_n'); - late final _options_add_attachment_n = - _options_add_attachment_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Sets the path to the crashpad handler if the crashpad backend is used. - /// - /// The path defaults to the `crashpad_handler`/`crashpad_handler.exe` - /// executable, depending on platform, which is expected to be present in the - /// same directory as the app executable. - /// - /// It is recommended that library users set an explicit handler path, depending - /// on the directory/executable structure of their app. - /// - /// `path` is assumed to be in platform-specific filesystem path encoding. - /// API Users on windows are encouraged to use `sentry_options_set_handler_pathw` - /// instead. - void options_set_handler_path( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_set_handler_path( - opts, - path, - ); - } - - late final _options_set_handler_pathPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_handler_path'); - late final _options_set_handler_path = - _options_set_handler_pathPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void options_set_handler_path_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_set_handler_path_n( - opts, - path, - path_len, - ); - } - - late final _options_set_handler_path_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_handler_path_n'); - late final _options_set_handler_path_n = - _options_set_handler_path_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Sets the path to the Sentry Database Directory. - /// - /// Sentry will use this path to persist user consent, sessions, and other - /// artifacts in case of a crash. This will also be used by the crashpad backend - /// if it is configured. - /// - /// The directory is used for "cached" data, which needs to persist across - /// application restarts to ensure proper flagging of release-health sessions, - /// but might otherwise be safely purged regularly. - /// - /// It is roughly equivalent to the type of `AppData/Local` on Windows and - /// `XDG_CACHE_HOME` on Linux, and equivalent runtime directories on other - /// platforms. - /// - /// It is recommended that users set an explicit absolute path, depending - /// on their apps runtime directory. The path will be created if it does not - /// exist, and will be resolved to an absolute path inside of `sentry_init`. The - /// directory should not be shared with other application data/configuration, as - /// sentry-native will enumerate and possibly delete files in that directory. An - /// example might be `$XDG_CACHE_HOME/your-app/sentry` - /// - /// If no explicit path it set, sentry-native will default to `.sentry-native` in - /// the current working directory, with no specific platform-specific handling. - /// - /// `path` is assumed to be in platform-specific filesystem path encoding. - /// API Users on windows are encouraged to use - /// `sentry_options_set_database_pathw` instead. - void options_set_database_path( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_set_database_path( - opts, - path, - ); - } - - late final _options_set_database_pathPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_database_path'); - late final _options_set_database_path = - _options_set_database_pathPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void options_set_database_path_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_set_database_path_n( - opts, - path, - path_len, - ); - } - - late final _options_set_database_path_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_database_path_n'); - late final _options_set_database_path_n = - _options_set_database_path_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Wide char version of `sentry_options_add_attachment`. - void options_add_attachmentw( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_add_attachmentw( - opts, - path, - ); - } - - late final _options_add_attachmentwPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_add_attachmentw'); - late final _options_add_attachmentw = _options_add_attachmentwPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void options_add_attachmentw_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_add_attachmentw_n( - opts, - path, - path_len, - ); - } - - late final _options_add_attachmentw_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_add_attachmentw_n'); - late final _options_add_attachmentw_n = - _options_add_attachmentw_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Wide char version of `sentry_options_set_handler_path`. - void options_set_handler_pathw( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_set_handler_pathw( - opts, - path, - ); - } - - late final _options_set_handler_pathwPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_handler_pathw'); - late final _options_set_handler_pathw = - _options_set_handler_pathwPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void options_set_handler_pathw_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_set_handler_pathw_n( - opts, - path, - path_len, - ); - } - - late final _options_set_handler_pathw_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_handler_pathw_n'); - late final _options_set_handler_pathw_n = - _options_set_handler_pathw_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Wide char version of `sentry_options_set_database_path`. - void options_set_database_pathw( - ffi.Pointer opts, - ffi.Pointer path, - ) { - return _options_set_database_pathw( - opts, - path, - ); - } - - late final _options_set_database_pathwPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_database_pathw'); - late final _options_set_database_pathw = - _options_set_database_pathwPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void options_set_database_pathw_n( - ffi.Pointer opts, - ffi.Pointer path, - int path_len, - ) { - return _options_set_database_pathw_n( - opts, - path, - path_len, - ); - } - - late final _options_set_database_pathw_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_options_set_database_pathw_n'); - late final _options_set_database_pathw_n = - _options_set_database_pathw_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Enables forwarding to the system crash reporter. Disabled by default. - /// - /// This setting only has an effect when using Crashpad on macOS. If enabled, - /// Crashpad forwards crashes to the macOS system crash reporter. Depending - /// on the crash, this may impact the crash time. Even if enabled, Crashpad - /// may choose not to forward certain crashes. - void options_set_system_crash_reporter_enabled( - ffi.Pointer opts, - int enabled, - ) { - return _options_set_system_crash_reporter_enabled( - opts, - enabled, - ); - } - - late final _options_set_system_crash_reporter_enabledPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_options_set_system_crash_reporter_enabled'); - late final _options_set_system_crash_reporter_enabled = - _options_set_system_crash_reporter_enabledPtr - .asFunction, int)>(); - - /// Sets the maximum time (in milliseconds) to wait for the asynchronous tasks to - /// end on shutdown, before attempting a forced termination. - void options_set_shutdown_timeout( - ffi.Pointer opts, - int shutdown_timeout, - ) { - return _options_set_shutdown_timeout( - opts, - shutdown_timeout, - ); - } - - late final _options_set_shutdown_timeoutPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Uint64)>>('sentry_options_set_shutdown_timeout'); - late final _options_set_shutdown_timeout = _options_set_shutdown_timeoutPtr - .asFunction, int)>(); - - /// Gets the maximum time (in milliseconds) to wait for the asynchronous tasks to - /// end on shutdown, before attempting a forced termination. - int options_get_shutdown_timeout( - ffi.Pointer opts, - ) { - return _options_get_shutdown_timeout( - opts, - ); - } - - late final _options_get_shutdown_timeoutPtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_options_get_shutdown_timeout'); - late final _options_get_shutdown_timeout = _options_get_shutdown_timeoutPtr - .asFunction)>(); - - /// Sets a user-defined backend. - /// - /// Since creation and destruction of backends is not exposed in the API, this - /// can only be used to set the backend to `NULL`, which disables the backend in - /// the initialization. - void options_set_backend( - ffi.Pointer opts, - ffi.Pointer backend, - ) { - return _options_set_backend( - opts, - backend, - ); - } - - late final _options_set_backendPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_options_set_backend'); - late final _options_set_backend = _options_set_backendPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - /// Initializes the Sentry SDK with the specified options. - /// - /// This takes ownership of the options. After the options have been set - /// they cannot be modified any more. - /// Depending on the configured transport and backend, this function might not be - /// fully thread-safe. - /// Returns 0 on success. - int init( - ffi.Pointer options, - ) { - return _init( - options, - ); - } - - late final _initPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_init'); - late final _init = - _initPtr.asFunction)>(); - - /// Instructs the transport to flush its send queue. - /// - /// The `timeout` parameter is in milliseconds. - /// - /// Returns 0 on success, or a non-zero return value in case the timeout is hit. - /// - /// Note that this function will block the thread it was called from until the - /// sentry background worker has finished its work or it timed out, whichever - /// comes first. - int flush( - int timeout, - ) { - return _flush( - timeout, - ); - } - - late final _flushPtr = - _lookup>('sentry_flush'); - late final _flush = _flushPtr.asFunction(); - - /// Shuts down the sentry client and forces transports to flush out. - /// - /// Returns 0 on success. - /// - /// Note that this does not uninstall any crash handler installed by our - /// backends, which will still process crashes after `sentry_close()`, except - /// when using `crashpad` on Linux or the `inproc` backend. - /// - /// Further note that this function will block the thread it was called from - /// until the sentry background worker has finished its work or it timed out, - /// whichever comes first. - int close() { - return _close(); - } - - late final _closePtr = - _lookup>('sentry_close'); - late final _close = _closePtr.asFunction(); - - /// Shuts down the sentry client and forces transports to flush out. - /// - /// This is a **deprecated** alias for `sentry_close`. - /// - /// Returns 0 on success. - int shutdown() { - return _shutdown(); - } - - late final _shutdownPtr = - _lookup>('sentry_shutdown'); - late final _shutdown = _shutdownPtr.asFunction(); - - /// This will lazily load and cache a list of all the loaded libraries. - /// - /// Returns a new reference to an immutable, frozen list. - /// The reference must be released with `sentry_value_decref`. - sentry_value_u get_modules_list() { - return _get_modules_list(); - } - - late final _get_modules_listPtr = - _lookup>( - 'sentry_get_modules_list'); - late final _get_modules_list = - _get_modules_listPtr.asFunction(); - - /// Clears the internal module cache. - /// - /// For performance reasons, sentry will cache the list of loaded libraries when - /// capturing events. This cache can get out-of-date when loading or unloading - /// libraries at runtime. It is therefore recommended to call - /// `sentry_clear_modulecache` when doing so, to make sure that the next call to - /// `sentry_capture_event` will have an up-to-date module list. - void clear_modulecache() { - return _clear_modulecache(); - } - - late final _clear_modulecachePtr = - _lookup>( - 'sentry_clear_modulecache'); - late final _clear_modulecache = - _clear_modulecachePtr.asFunction(); - - /// Re-initializes the Sentry backend. - /// - /// This is needed if a third-party library overrides the previously installed - /// signal handler. Calling this function can be potentially dangerous and should - /// only be done when necessary. - /// - /// Returns 0 on success. - int reinstall_backend() { - return _reinstall_backend(); - } - - late final _reinstall_backendPtr = - _lookup>( - 'sentry_reinstall_backend'); - late final _reinstall_backend = - _reinstall_backendPtr.asFunction(); - - /// Gives user consent. - void user_consent_give() { - return _user_consent_give(); - } - - late final _user_consent_givePtr = - _lookup>( - 'sentry_user_consent_give'); - late final _user_consent_give = - _user_consent_givePtr.asFunction(); - - /// Revokes user consent. - void user_consent_revoke() { - return _user_consent_revoke(); - } - - late final _user_consent_revokePtr = - _lookup>( - 'sentry_user_consent_revoke'); - late final _user_consent_revoke = - _user_consent_revokePtr.asFunction(); - - /// Resets the user consent (back to unknown). - void user_consent_reset() { - return _user_consent_reset(); - } - - late final _user_consent_resetPtr = - _lookup>( - 'sentry_user_consent_reset'); - late final _user_consent_reset = - _user_consent_resetPtr.asFunction(); - - /// Checks the current state of user consent. - int user_consent_get() { - return _user_consent_get(); - } - - late final _user_consent_getPtr = - _lookup>( - 'sentry_user_consent_get'); - late final _user_consent_get = - _user_consent_getPtr.asFunction(); - - /// Sends a sentry event. - /// - /// If returns a nil UUID if the event being passed in is a transaction, and the - /// transaction will not be sent nor consumed. `sentry_transaction_finish` should - /// be used to send transactions. - sentry_uuid_s capture_event( - sentry_value_u event, - ) { - return _capture_event( - event, - ); - } - - late final _capture_eventPtr = - _lookup>( - 'sentry_capture_event'); - late final _capture_event = - _capture_eventPtr.asFunction(); - - /// Captures an exception to be handled by the backend. - /// - /// This is safe to be called from a crashing thread and may not return. - void handle_exception( - ffi.Pointer uctx, - ) { - return _handle_exception( - uctx, - ); - } - - late final _handle_exceptionPtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_handle_exception'); - late final _handle_exception = _handle_exceptionPtr - .asFunction)>(); - - /// Adds the breadcrumb to be sent in case of an event. - void add_breadcrumb( - sentry_value_u breadcrumb, - ) { - return _add_breadcrumb( - breadcrumb, - ); - } - - late final _add_breadcrumbPtr = - _lookup>( - 'sentry_add_breadcrumb'); - late final _add_breadcrumb = - _add_breadcrumbPtr.asFunction(); - - /// Sets the specified user. - void set_user( - sentry_value_u user, - ) { - return _set_user( - user, - ); - } - - late final _set_userPtr = - _lookup>( - 'sentry_set_user'); - late final _set_user = - _set_userPtr.asFunction(); - - /// Removes a user. - void remove_user() { - return _remove_user(); - } - - late final _remove_userPtr = - _lookup>('sentry_remove_user'); - late final _remove_user = _remove_userPtr.asFunction(); - - /// Sets a tag. - void set_tag( - ffi.Pointer key, - ffi.Pointer value, - ) { - return _set_tag( - key, - value, - ); - } - - late final _set_tagPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, ffi.Pointer)>>('sentry_set_tag'); - late final _set_tag = _set_tagPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void set_tag_n( - ffi.Pointer key, - int key_len, - ffi.Pointer value, - int value_len, - ) { - return _set_tag_n( - key, - key_len, - value, - value_len, - ); - } - - late final _set_tag_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Size, - ffi.Pointer, ffi.Size)>>('sentry_set_tag_n'); - late final _set_tag_n = _set_tag_nPtr.asFunction< - void Function(ffi.Pointer, int, ffi.Pointer, int)>(); - - /// Removes the tag with the specified key. - void remove_tag( - ffi.Pointer key, - ) { - return _remove_tag( - key, - ); - } - - late final _remove_tagPtr = - _lookup)>>( - 'sentry_remove_tag'); - late final _remove_tag = - _remove_tagPtr.asFunction)>(); - - void remove_tag_n( - ffi.Pointer key, - int key_len, - ) { - return _remove_tag_n( - key, - key_len, - ); - } - - late final _remove_tag_nPtr = _lookup< - ffi - .NativeFunction, ffi.Size)>>( - 'sentry_remove_tag_n'); - late final _remove_tag_n = - _remove_tag_nPtr.asFunction, int)>(); - - /// Sets extra information. - void set_extra( - ffi.Pointer key, - sentry_value_u value, - ) { - return _set_extra( - key, - value, - ); - } - - late final _set_extraPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, sentry_value_u)>>('sentry_set_extra'); - late final _set_extra = _set_extraPtr - .asFunction, sentry_value_u)>(); - - void set_extra_n( - ffi.Pointer key, - int key_len, - sentry_value_u value, - ) { - return _set_extra_n( - key, - key_len, - value, - ); - } - - late final _set_extra_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Size, - sentry_value_u)>>('sentry_set_extra_n'); - late final _set_extra_n = _set_extra_nPtr - .asFunction, int, sentry_value_u)>(); - - /// Removes the extra with the specified key. - void remove_extra( - ffi.Pointer key, - ) { - return _remove_extra( - key, - ); - } - - late final _remove_extraPtr = - _lookup)>>( - 'sentry_remove_extra'); - late final _remove_extra = - _remove_extraPtr.asFunction)>(); - - void remove_extra_n( - ffi.Pointer key, - int key_len, - ) { - return _remove_extra_n( - key, - key_len, - ); - } - - late final _remove_extra_nPtr = _lookup< - ffi - .NativeFunction, ffi.Size)>>( - 'sentry_remove_extra_n'); - late final _remove_extra_n = _remove_extra_nPtr - .asFunction, int)>(); - - /// Sets a context object. - void set_context( - ffi.Pointer key, - sentry_value_u value, - ) { - return _set_context( - key, - value, - ); - } - - late final _set_contextPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, sentry_value_u)>>('sentry_set_context'); - late final _set_context = _set_contextPtr - .asFunction, sentry_value_u)>(); - - void set_context_n( - ffi.Pointer key, - int key_len, - sentry_value_u value, - ) { - return _set_context_n( - key, - key_len, - value, - ); - } - - late final _set_context_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Size, - sentry_value_u)>>('sentry_set_context_n'); - late final _set_context_n = _set_context_nPtr - .asFunction, int, sentry_value_u)>(); - - /// Removes the context object with the specified key. - void remove_context( - ffi.Pointer key, - ) { - return _remove_context( - key, - ); - } - - late final _remove_contextPtr = - _lookup)>>( - 'sentry_remove_context'); - late final _remove_context = - _remove_contextPtr.asFunction)>(); - - void remove_context_n( - ffi.Pointer key, - int key_len, - ) { - return _remove_context_n( - key, - key_len, - ); - } - - late final _remove_context_nPtr = _lookup< - ffi - .NativeFunction, ffi.Size)>>( - 'sentry_remove_context_n'); - late final _remove_context_n = _remove_context_nPtr - .asFunction, int)>(); - - /// Sets the event fingerprint. - /// - /// This accepts a variable number of arguments, and needs to be terminated by a - /// trailing `NULL`. - void set_fingerprint( - ffi.Pointer fingerprint, - ) { - return _set_fingerprint( - fingerprint, - ); - } - - late final _set_fingerprintPtr = - _lookup)>>( - 'sentry_set_fingerprint'); - late final _set_fingerprint = - _set_fingerprintPtr.asFunction)>(); - - void set_fingerprint_n( - ffi.Pointer fingerprint, - int fingerprint_len, - ) { - return _set_fingerprint_n( - fingerprint, - fingerprint_len, - ); - } - - late final _set_fingerprint_nPtr = _lookup< - ffi - .NativeFunction, ffi.Size)>>( - 'sentry_set_fingerprint_n'); - late final _set_fingerprint_n = _set_fingerprint_nPtr - .asFunction, int)>(); - - /// Removes the fingerprint. - void remove_fingerprint() { - return _remove_fingerprint(); - } - - late final _remove_fingerprintPtr = - _lookup>( - 'sentry_remove_fingerprint'); - late final _remove_fingerprint = - _remove_fingerprintPtr.asFunction(); - - /// Sets the transaction. - void set_transaction( - ffi.Pointer transaction, - ) { - return _set_transaction( - transaction, - ); - } - - late final _set_transactionPtr = - _lookup)>>( - 'sentry_set_transaction'); - late final _set_transaction = - _set_transactionPtr.asFunction)>(); - - void set_transaction_n( - ffi.Pointer transaction, - int transaction_len, - ) { - return _set_transaction_n( - transaction, - transaction_len, - ); - } - - late final _set_transaction_nPtr = _lookup< - ffi - .NativeFunction, ffi.Size)>>( - 'sentry_set_transaction_n'); - late final _set_transaction_n = _set_transaction_nPtr - .asFunction, int)>(); - - /// Sets the event level. - void set_level( - int level, - ) { - return _set_level( - level, - ); - } - - late final _set_levelPtr = - _lookup>( - 'sentry_set_level'); - late final _set_level = _set_levelPtr.asFunction(); - - /// Sets the maximum number of spans that can be attached to a - /// transaction. - void options_set_max_spans( - ffi.Pointer opts, - int max_spans, - ) { - return _options_set_max_spans( - opts, - max_spans, - ); - } - - late final _options_set_max_spansPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Size)>>('sentry_options_set_max_spans'); - late final _options_set_max_spans = _options_set_max_spansPtr - .asFunction, int)>(); - - /// Gets the maximum number of spans that can be attached to a - /// transaction. - int options_get_max_spans( - ffi.Pointer opts, - ) { - return _options_get_max_spans( - opts, - ); - } - - late final _options_get_max_spansPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_options_get_max_spans'); - late final _options_get_max_spans = _options_get_max_spansPtr - .asFunction)>(); - - /// Sets the sample rate for transactions. Should be a double between - /// `0.0` and `1.0`. Transactions will be randomly discarded during - /// `sentry_transaction_finish` when the sample rate is < 1.0. - void options_set_traces_sample_rate( - ffi.Pointer opts, - double sample_rate, - ) { - return _options_set_traces_sample_rate( - opts, - sample_rate, - ); - } - - late final _options_set_traces_sample_ratePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Double)>>('sentry_options_set_traces_sample_rate'); - late final _options_set_traces_sample_rate = - _options_set_traces_sample_ratePtr - .asFunction, double)>(); - - /// Returns the sample rate for transactions. - double options_get_traces_sample_rate( - ffi.Pointer opts, - ) { - return _options_get_traces_sample_rate( - opts, - ); - } - - late final _options_get_traces_sample_ratePtr = _lookup< - ffi - .NativeFunction)>>( - 'sentry_options_get_traces_sample_rate'); - late final _options_get_traces_sample_rate = - _options_get_traces_sample_ratePtr - .asFunction)>(); - - /// Starts a new session. - void start_session() { - return _start_session(); - } - - late final _start_sessionPtr = - _lookup>('sentry_start_session'); - late final _start_session = _start_sessionPtr.asFunction(); - - /// Ends a session. - void end_session() { - return _end_session(); - } - - late final _end_sessionPtr = - _lookup>('sentry_end_session'); - late final _end_session = _end_sessionPtr.asFunction(); - - /// Ends a session with an explicit `status` code. - void end_session_with_status( - int status, - ) { - return _end_session_with_status( - status, - ); - } - - late final _end_session_with_statusPtr = - _lookup>( - 'sentry_end_session_with_status'); - late final _end_session_with_status = - _end_session_with_statusPtr.asFunction(); - - /// Constructs a new Transaction Context. The returned value needs to be passed - /// into `sentry_transaction_start` in order to be recorded and sent to sentry. - /// - /// See - /// https://docs.sentry.io/platforms/native/enriching-events/transaction-name/ - /// for an explanation of a Transaction's `name`, and - /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions - /// around an `operation`'s value. - /// - /// Also see https://develop.sentry.dev/sdk/event-payloads/transaction/#anatomy - /// for an explanation of `operation`, in addition to other properties and - /// actions that can be performed on a Transaction. - /// - /// The returned value is not thread-safe. Users are expected to ensure that - /// appropriate locking mechanisms are implemented over the Transaction Context - /// if it needs to be mutated across threads. Methods operating on the - /// Transaction Context will mention what kind of expectations they carry if they - /// need to mutate or access the object in a thread-safe way. - ffi.Pointer transaction_context_new( - ffi.Pointer name, - ffi.Pointer operation, - ) { - return _transaction_context_new( - name, - operation, - ); - } - - late final _transaction_context_newPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_context_new'); - late final _transaction_context_new = _transaction_context_newPtr.asFunction< - ffi.Pointer Function( - ffi.Pointer, ffi.Pointer)>(); - - ffi.Pointer transaction_context_new_n( - ffi.Pointer name, - int name_len, - ffi.Pointer operation, - int operation_len, - ) { - return _transaction_context_new_n( - name, - name_len, - operation, - operation_len, - ); - } - - late final _transaction_context_new_nPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_context_new_n'); - late final _transaction_context_new_n = - _transaction_context_new_nPtr.asFunction< - ffi.Pointer Function( - ffi.Pointer, int, ffi.Pointer, int)>(); - - /// Sets the `name` on a Transaction Context, which will be used in the - /// Transaction constructed off of the context. - /// - /// The Transaction Context should not be mutated by other functions while - /// setting a name on it. - void transaction_context_set_name( - ffi.Pointer tx_cxt, - ffi.Pointer name, - ) { - return _transaction_context_set_name( - tx_cxt, - name, - ); - } - - late final _transaction_context_set_namePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_context_set_name'); - late final _transaction_context_set_name = - _transaction_context_set_namePtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer)>(); - - void transaction_context_set_name_n( - ffi.Pointer tx_cxt, - ffi.Pointer name, - int name_len, - ) { - return _transaction_context_set_name_n( - tx_cxt, - name, - name_len, - ); - } - - late final _transaction_context_set_name_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_context_set_name_n'); - late final _transaction_context_set_name_n = - _transaction_context_set_name_nPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer, int)>(); - - /// Sets the `operation` on a Transaction Context, which will be used in the - /// Transaction constructed off of the context - /// - /// See https://develop.sentry.dev/sdk/performance/span-operations/ for - /// conventions on `operation`s. - /// - /// The Transaction Context should not be mutated by other functions while - /// setting an operation on it. - void transaction_context_set_operation( - ffi.Pointer tx_cxt, - ffi.Pointer operation, - ) { - return _transaction_context_set_operation( - tx_cxt, - operation, - ); - } - - late final _transaction_context_set_operationPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>( - 'sentry_transaction_context_set_operation'); - late final _transaction_context_set_operation = - _transaction_context_set_operationPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer)>(); - - void transaction_context_set_operation_n( - ffi.Pointer tx_cxt, - ffi.Pointer operation, - int operation_len, - ) { - return _transaction_context_set_operation_n( - tx_cxt, - operation, - operation_len, - ); - } - - late final _transaction_context_set_operation_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_context_set_operation_n'); - late final _transaction_context_set_operation_n = - _transaction_context_set_operation_nPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer, int)>(); - - /// Sets the `sampled` field on a Transaction Context, which will be used in the - /// Transaction constructed off of the context. - /// - /// When passed any value above 0, the Transaction will bypass all sampling - /// options and always be sent to sentry. If passed 0, this Transaction and its - /// child spans will never be sent to sentry. - /// - /// The Transaction Context should not be mutated by other functions while - /// setting `sampled` on it. - void transaction_context_set_sampled( - ffi.Pointer tx_cxt, - int sampled, - ) { - return _transaction_context_set_sampled( - tx_cxt, - sampled, - ); - } - - late final _transaction_context_set_sampledPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int)>>('sentry_transaction_context_set_sampled'); - late final _transaction_context_set_sampled = - _transaction_context_set_sampledPtr.asFunction< - void Function(ffi.Pointer, int)>(); - - /// Removes the `sampled` field on a Transaction Context, which will be used in - /// the Transaction constructed off of the context. - /// - /// The Transaction will use the sampling rate as defined in `sentry_options`. - /// - /// The Transaction Context should not be mutated by other functions while - /// removing `sampled`. - void transaction_context_remove_sampled( - ffi.Pointer tx_cxt, - ) { - return _transaction_context_remove_sampled( - tx_cxt, - ); - } - - late final _transaction_context_remove_sampledPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer)>>( - 'sentry_transaction_context_remove_sampled'); - late final _transaction_context_remove_sampled = - _transaction_context_remove_sampledPtr.asFunction< - void Function(ffi.Pointer)>(); - - /// Update the Transaction Context with the given HTTP header key/value pair. - /// - /// This is used to propagate distributed tracing metadata from upstream - /// services. Therefore, the headers of incoming requests should be fed into this - /// function so that sentry is able to continue a trace that was started by an - /// upstream service. - void transaction_context_update_from_header( - ffi.Pointer tx_cxt, - ffi.Pointer key, - ffi.Pointer value, - ) { - return _transaction_context_update_from_header( - tx_cxt, - key, - value, - ); - } - - late final _transaction_context_update_from_headerPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>( - 'sentry_transaction_context_update_from_header'); - late final _transaction_context_update_from_header = - _transaction_context_update_from_headerPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - void transaction_context_update_from_header_n( - ffi.Pointer tx_cxt, - ffi.Pointer key, - int key_len, - ffi.Pointer value, - int value_len, - ) { - return _transaction_context_update_from_header_n( - tx_cxt, - key, - key_len, - value, - value_len, - ); - } - - late final _transaction_context_update_from_header_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_context_update_from_header_n'); - late final _transaction_context_update_from_header_n = - _transaction_context_update_from_header_nPtr.asFunction< - void Function(ffi.Pointer, - ffi.Pointer, int, ffi.Pointer, int)>(); - - /// Starts a new Transaction based on the provided context, restored from an - /// external integration (i.e. a span from a different SDK) or manually - /// constructed by a user. - /// - /// The second parameter is a custom Sampling Context to be used with a Traces - /// Sampler to make a more informed sampling decision. The SDK does not currently - /// support a custom Traces Sampler and this parameter is ignored for the time - /// being but needs to be provided. - /// - /// Returns a Transaction, which is expected to be manually managed by the - /// caller. Manual management involves ensuring that `sentry_transaction_finish` - /// is invoked for the Transaction, and that the caller manually starts and - /// finishes any child Spans as needed on the Transaction. - /// - /// Not invoking `sentry_transaction_finish` with the returned Transaction means - /// it will be discarded, and will not be sent to sentry. - /// - /// To ensure that any Events or Message Events are associated with this - /// Transaction while it is active, invoke and pass in the Transaction returned - /// by this function to `sentry_set_transaction_object`. Further documentation on - /// this can be found in `sentry_set_transaction_object`'s docstring. - /// - /// Takes ownership of `transaction_context`. A Transaction Context cannot be - /// modified or re-used after it is used to start a Transaction. - /// - /// The returned value is not thread-safe. Users are expected to ensure that - /// appropriate locking mechanisms are implemented over the Transaction if it - /// needs to be mutated across threads. Methods operating on the Transaction will - /// mention what kind of expectations they carry if they need to mutate or access - /// the object in a thread-safe way. - ffi.Pointer transaction_start( - ffi.Pointer tx_cxt, - sentry_value_u sampling_ctx, - ) { - return _transaction_start( - tx_cxt, - sampling_ctx, - ); - } - - late final _transaction_startPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - sentry_value_u)>>('sentry_transaction_start'); - late final _transaction_start = _transaction_startPtr.asFunction< - ffi.Pointer Function( - ffi.Pointer, sentry_value_u)>(); - - /// Finishes and sends a Transaction to sentry. The event ID of the Transaction - /// will be returned if this was successful; A nil UUID will be returned - /// otherwise. - /// - /// Always takes ownership of `transaction`, regardless of whether the operation - /// was successful or not. A Transaction cannot be modified or re-used after it - /// is finished. - sentry_uuid_s transaction_finish( - ffi.Pointer tx, - ) { - return _transaction_finish( - tx, - ); - } - - late final _transaction_finishPtr = _lookup< - ffi.NativeFunction< - sentry_uuid_s Function( - ffi.Pointer)>>('sentry_transaction_finish'); - late final _transaction_finish = _transaction_finishPtr - .asFunction)>(); - - /// Sets the Transaction so any Events sent while the Transaction - /// is active will be associated with the Transaction. - /// - /// If the Transaction being passed in is unsampled, it will still be associated - /// with any new Events. This will lead to some Events pointing to orphan or - /// missing traces in sentry, see - /// https://docs.sentry.io/product/sentry-basics/tracing/trace-view/#orphan-traces-and-broken-subtraces - /// - /// This increases the number of references pointing to the Transaction. Invoke - /// `sentry_transaction_finish` to remove the Transaction set by this function as - /// well as its reference by passing in the same Transaction as the one passed - /// into this function. - void set_transaction_object( - ffi.Pointer tx, - ) { - return _set_transaction_object( - tx, - ); - } - - late final _set_transaction_objectPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer)>>( - 'sentry_set_transaction_object'); - late final _set_transaction_object = _set_transaction_objectPtr - .asFunction)>(); - - /// Sets the Span so any Events sent while the Span - /// is active will be associated with the Span. - /// - /// This increases the number of references pointing to the Span. Invoke - /// `sentry_span_finish` to remove the Span set by this function as well - /// as its reference by passing in the same Span as the one passed into - /// this function. - void set_span( - ffi.Pointer span, - ) { - return _set_span( - span, - ); - } - - late final _set_spanPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_set_span'); - late final _set_span = - _set_spanPtr.asFunction)>(); - - /// Starts a new Span. - /// - /// The return value of `sentry_transaction_start` should be passed in as - /// `parent`. - /// - /// Both `operation` and `description` can be null, but it is recommended to - /// supply the former. See - /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions - /// around operations. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/span/ for a description of - /// the created Span's properties and expectations for `operation` and - /// `description`. - /// - /// Returns a value that should be passed into `sentry_span_finish`. Not - /// finishing the Span means it will be discarded, and will not be sent to - /// sentry. `sentry_value_null` will be returned if the child Span could not be - /// created. - /// - /// To ensure that any Events or Message Events are associated with this - /// Span while it is active, invoke and pass in the Span returned - /// by this function to `sentry_set_span`. Further documentation on this can be - /// found in `sentry_set_span`'s docstring. - /// - /// This increases the number of references pointing to the Transaction. - /// - /// The returned value is not thread-safe. Users are expected to ensure that - /// appropriate locking mechanisms are implemented over the Span if it needs - /// to be mutated across threads. Methods operating on the Span will mention what - /// kind of expectations they carry if they need to mutate or access the object - /// in a thread-safe way. - ffi.Pointer transaction_start_child( - ffi.Pointer parent, - ffi.Pointer operation, - ffi.Pointer description, - ) { - return _transaction_start_child( - parent, - operation, - description, - ); - } - - late final _transaction_start_childPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_start_child'); - late final _transaction_start_child = _transaction_start_childPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - ffi.Pointer transaction_start_child_n( - ffi.Pointer parent, - ffi.Pointer operation, - int operation_len, - ffi.Pointer description, - int description_len, - ) { - return _transaction_start_child_n( - parent, - operation, - operation_len, - description, - description_len, - ); - } - - late final _transaction_start_child_nPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_start_child_n'); - late final _transaction_start_child_n = - _transaction_start_child_nPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer, int, ffi.Pointer, int)>(); - - /// Starts a new Span. - /// - /// The return value of `sentry_span_start_child` may be passed in as `parent`. - /// - /// Both `operation` and `description` can be null, but it is recommended to - /// supply the former. See - /// https://develop.sentry.dev/sdk/performance/span-operations/ for conventions - /// around operations. - /// - /// See https://develop.sentry.dev/sdk/event-payloads/span/ for a description of - /// the created Span's properties and expectations for `operation` and - /// `description`. - /// - /// Returns a value that should be passed into `sentry_span_finish`. Not - /// finishing the Span means it will be discarded, and will not be sent to - /// sentry. `sentry_value_null` will be returned if the child Span could not be - /// created. - /// - /// To ensure that any Events or Message Events are associated with this - /// Span while it is active, invoke and pass in the Span returned - /// by this function to `sentry_set_span`. Further documentation on this can be - /// found in `sentry_set_span`'s docstring. - /// - /// The returned value is not thread-safe. Users are expected to ensure that - /// appropriate locking mechanisms are implemented over the Span if it needs - /// to be mutated across threads. Methods operating on the Span will mention what - /// kind of expectations they carry if they need to mutate or access the object - /// in a thread-safe way. - ffi.Pointer span_start_child( - ffi.Pointer parent, - ffi.Pointer operation, - ffi.Pointer description, - ) { - return _span_start_child( - parent, - operation, - description, - ); - } - - late final _span_start_childPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('sentry_span_start_child'); - late final _span_start_child = _span_start_childPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - ffi.Pointer span_start_child_n( - ffi.Pointer parent, - ffi.Pointer operation, - int operation_len, - ffi.Pointer description, - int description_len, - ) { - return _span_start_child_n( - parent, - operation, - operation_len, - description, - description_len, - ); - } - - late final _span_start_child_nPtr = _lookup< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_span_start_child_n'); - late final _span_start_child_n = _span_start_child_nPtr.asFunction< - ffi.Pointer Function(ffi.Pointer, - ffi.Pointer, int, ffi.Pointer, int)>(); - - /// Finishes a Span. - /// - /// This takes ownership of `span`. A Span cannot be modified or re-used after it - /// is finished. - /// - /// This will mutate the `span`'s containing Transaction, so the containing - /// Transaction should also not be mutated by other functions when finishing a - /// span. - void span_finish( - ffi.Pointer span, - ) { - return _span_finish( - span, - ); - } - - late final _span_finishPtr = _lookup< - ffi.NativeFunction)>>( - 'sentry_span_finish'); - late final _span_finish = - _span_finishPtr.asFunction)>(); - - /// Sets a tag on a Transaction to the given string value. - /// - /// Tags longer than 200 bytes will be truncated. - /// - /// The Transaction should not be mutated by other functions while a tag is being - /// set on it. - void transaction_set_tag( - ffi.Pointer transaction, - ffi.Pointer tag, - ffi.Pointer value, - ) { - return _transaction_set_tag( - transaction, - tag, - value, - ); - } - - late final _transaction_set_tagPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_set_tag'); - late final _transaction_set_tag = _transaction_set_tagPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>(); - - void transaction_set_tag_n( - ffi.Pointer transaction, - ffi.Pointer tag, - int tag_len, - ffi.Pointer value, - int value_len, - ) { - return _transaction_set_tag_n( - transaction, - tag, - tag_len, - value, - value_len, - ); - } - - late final _transaction_set_tag_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_set_tag_n'); - late final _transaction_set_tag_n = _transaction_set_tag_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, - int, ffi.Pointer, int)>(); - - /// Removes a tag from a Transaction. - /// - /// The Transaction should not be mutated by other functions while a tag is being - /// removed from it. - void transaction_remove_tag( - ffi.Pointer transaction, - ffi.Pointer tag, - ) { - return _transaction_remove_tag( - transaction, - tag, - ); - } - - late final _transaction_remove_tagPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_remove_tag'); - late final _transaction_remove_tag = _transaction_remove_tagPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void transaction_remove_tag_n( - ffi.Pointer transaction, - ffi.Pointer tag, - int tag_len, - ) { - return _transaction_remove_tag_n( - transaction, - tag, - tag_len, - ); - } - - late final _transaction_remove_tag_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_remove_tag_n'); - late final _transaction_remove_tag_n = - _transaction_remove_tag_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Sets the given key in a Transaction's "data" section to the given value. - /// - /// The Transaction should not be mutated by other functions while data is being - /// set on it. - void transaction_set_data( - ffi.Pointer transaction, - ffi.Pointer key, - sentry_value_u value, - ) { - return _transaction_set_data( - transaction, - key, - value, - ); - } - - late final _transaction_set_dataPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - sentry_value_u)>>('sentry_transaction_set_data'); - late final _transaction_set_data = _transaction_set_dataPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, - sentry_value_u)>(); - - void transaction_set_data_n( - ffi.Pointer transaction, - ffi.Pointer key, - int key_len, - sentry_value_u value, - ) { - return _transaction_set_data_n( - transaction, - key, - key_len, - value, - ); - } - - late final _transaction_set_data_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - sentry_value_u)>>('sentry_transaction_set_data_n'); - late final _transaction_set_data_n = _transaction_set_data_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, - int, sentry_value_u)>(); - - /// Removes a key from a Transaction's "data" section. - /// - /// The Transaction should not be mutated by other functions while data is being - /// removed from it. - void transaction_remove_data( - ffi.Pointer transaction, - ffi.Pointer key, - ) { - return _transaction_remove_data( - transaction, - key, - ); - } - - late final _transaction_remove_dataPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_remove_data'); - late final _transaction_remove_data = _transaction_remove_dataPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); - - void transaction_remove_data_n( - ffi.Pointer transaction, - ffi.Pointer key, - int key_len, - ) { - return _transaction_remove_data_n( - transaction, - key, - key_len, - ); - } - - late final _transaction_remove_data_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_remove_data_n'); - late final _transaction_remove_data_n = - _transaction_remove_data_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); - - /// Sets a tag on a Span to the given string value. - /// - /// Tags longer than 200 bytes will be truncated. - /// - /// The Span should not be mutated by other functions while a tag is being set on - /// it. - void span_set_tag( - ffi.Pointer span, - ffi.Pointer tag, - ffi.Pointer value, - ) { - return _span_set_tag( - span, - tag, - value, - ); - } - - late final _span_set_tagPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>>('sentry_span_set_tag'); - late final _span_set_tag = _span_set_tagPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>(); - - void span_set_tag_n( - ffi.Pointer span, - ffi.Pointer tag, - int tag_len, - ffi.Pointer value, - int value_len, - ) { - return _span_set_tag_n( - span, - tag, - tag_len, - value, - value_len, - ); - } - - late final _span_set_tag_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_span_set_tag_n'); - late final _span_set_tag_n = _span_set_tag_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int, - ffi.Pointer, int)>(); - - /// Removes a tag from a Span. - /// - /// The Span should not be mutated by other functions while a tag is being - /// removed from it. - void span_remove_tag( - ffi.Pointer span, - ffi.Pointer tag, - ) { - return _span_remove_tag( - span, - tag, - ); - } - - late final _span_remove_tagPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_span_remove_tag'); - late final _span_remove_tag = _span_remove_tagPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); - - void span_remove_tag_n( - ffi.Pointer span, - ffi.Pointer tag, - int tag_len, - ) { - return _span_remove_tag_n( - span, - tag, - tag_len, - ); - } - - late final _span_remove_tag_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Size)>>('sentry_span_remove_tag_n'); - late final _span_remove_tag_n = _span_remove_tag_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int)>(); - - /// Sets the given key in a Span's "data" section to the given value. - /// - /// The Span should not be mutated by other functions while data is being set on - /// it. - void span_set_data( - ffi.Pointer span, - ffi.Pointer key, - sentry_value_u value, - ) { - return _span_set_data( - span, - key, - value, - ); - } - - late final _span_set_dataPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - sentry_value_u)>>('sentry_span_set_data'); - late final _span_set_data = _span_set_dataPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, sentry_value_u)>(); - - void span_set_data_n( - ffi.Pointer span, - ffi.Pointer key, - int key_len, - sentry_value_u value, + /// Gets the number of breadcrumbs being tracked and attached to events. + int options_get_max_breadcrumbs( + ffi.Pointer opts, ) { - return _span_set_data_n( - span, - key, - key_len, - value, + return _options_get_max_breadcrumbs( + opts, ); } - late final _span_set_data_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Size, sentry_value_u)>>('sentry_span_set_data_n'); - late final _span_set_data_n = _span_set_data_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int, - sentry_value_u)>(); + late final _options_get_max_breadcrumbsPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_max_breadcrumbs'); + late final _options_get_max_breadcrumbs = _options_get_max_breadcrumbsPtr + .asFunction)>(); - /// Removes a key from a Span's "data" section. + /// Enables or disables automatic session tracking. /// - /// The Span should not be mutated by other functions while data is being removed - /// from it. - void span_remove_data( - ffi.Pointer span, - ffi.Pointer key, + /// Automatic session tracking is enabled by default and is equivalent to calling + /// `sentry_start_session` after startup. + /// There can only be one running session, and the current session will always be + /// closed implicitly by `sentry_close`, when starting a new session with + /// `sentry_start_session`, or manually by calling `sentry_end_session`. + void options_set_auto_session_tracking( + ffi.Pointer opts, + int val, ) { - return _span_remove_data( - span, - key, + return _options_set_auto_session_tracking( + opts, + val, ); } - late final _span_remove_dataPtr = _lookup< + late final _options_set_auto_session_trackingPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_span_remove_data'); - late final _span_remove_data = _span_remove_dataPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer)>(); + ffi.Void Function(ffi.Pointer, + ffi.Int)>>('sentry_options_set_auto_session_tracking'); + late final _options_set_auto_session_tracking = + _options_set_auto_session_trackingPtr + .asFunction, int)>(); - void span_remove_data_n( - ffi.Pointer span, - ffi.Pointer key, - int key_len, + /// Returns true if automatic session tracking is enabled. + int options_get_auto_session_tracking( + ffi.Pointer opts, ) { - return _span_remove_data_n( - span, - key, - key_len, + return _options_get_auto_session_tracking( + opts, ); } - late final _span_remove_data_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Size)>>('sentry_span_remove_data_n'); - late final _span_remove_data_n = _span_remove_data_nPtr.asFunction< - void Function(ffi.Pointer, ffi.Pointer, int)>(); + late final _options_get_auto_session_trackingPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_options_get_auto_session_tracking'); + late final _options_get_auto_session_tracking = + _options_get_auto_session_trackingPtr + .asFunction)>(); - /// Sets a Transaction's name. + /// Initializes the Sentry SDK with the specified options. /// - /// The Transaction should not be mutated by other functions while setting its - /// name. - void transaction_set_name( - ffi.Pointer transaction, - ffi.Pointer name, + /// This takes ownership of the options. After the options have been set + /// they cannot be modified any more. + /// Depending on the configured transport and backend, this function might not be + /// fully thread-safe. + /// Returns 0 on success. + int init( + ffi.Pointer options, ) { - return _transaction_set_name( - transaction, - name, + return _init( + options, ); } - late final _transaction_set_namePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer)>>('sentry_transaction_set_name'); - late final _transaction_set_name = _transaction_set_namePtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer)>(); + late final _initPtr = _lookup< + ffi.NativeFunction)>>( + 'sentry_init'); + late final _init = + _initPtr.asFunction)>(); - void transaction_set_name_n( - ffi.Pointer transaction, - ffi.Pointer name, - int name_len, - ) { - return _transaction_set_name_n( - transaction, - name, - name_len, - ); + /// Shuts down the sentry client and forces transports to flush out. + /// + /// Returns 0 on success. + /// + /// Note that this does not uninstall any crash handler installed by our + /// backends, which will still process crashes after `sentry_close()`, except + /// when using `crashpad` on Linux or the `inproc` backend. + /// + /// Further note that this function will block the thread it was called from + /// until the sentry background worker has finished its work or it timed out, + /// whichever comes first. + int close() { + return _close(); } - late final _transaction_set_name_nPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size)>>('sentry_transaction_set_name_n'); - late final _transaction_set_name_n = _transaction_set_name_nPtr.asFunction< - void Function( - ffi.Pointer, ffi.Pointer, int)>(); + late final _closePtr = + _lookup>('sentry_close'); + late final _close = _closePtr.asFunction(); - /// Creates a new User Feedback with a specific name, email and comments. - /// - /// See https://develop.sentry.dev/sdk/envelopes/#user-feedback + /// This will lazily load and cache a list of all the loaded libraries. /// - /// User Feedback has to be associated with a specific event that has been - /// sent to Sentry earlier. - sentry_value_u value_new_user_feedback( - ffi.Pointer uuid, - ffi.Pointer name, - ffi.Pointer email, - ffi.Pointer comments, - ) { - return _value_new_user_feedback( - uuid, - name, - email, - comments, - ); + /// Returns a new reference to an immutable, frozen list. + /// The reference must be released with `sentry_value_decref`. + sentry_value_u get_modules_list() { + return _get_modules_list(); } - late final _value_new_user_feedbackPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('sentry_value_new_user_feedback'); - late final _value_new_user_feedback = _value_new_user_feedbackPtr.asFunction< - sentry_value_u Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); + late final _get_modules_listPtr = + _lookup>( + 'sentry_get_modules_list'); + late final _get_modules_list = + _get_modules_listPtr.asFunction(); - sentry_value_u value_new_user_feedback_n( - ffi.Pointer uuid, - ffi.Pointer name, - int name_len, - ffi.Pointer email, - int email_len, - ffi.Pointer comments, - int comments_len, + /// Adds the breadcrumb to be sent in case of an event. + void add_breadcrumb( + sentry_value_u breadcrumb, ) { - return _value_new_user_feedback_n( - uuid, - name, - name_len, - email, - email_len, - comments, - comments_len, + return _add_breadcrumb( + breadcrumb, ); } - late final _value_new_user_feedback_nPtr = _lookup< - ffi.NativeFunction< - sentry_value_u Function( - ffi.Pointer, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size, - ffi.Pointer, - ffi.Size)>>('sentry_value_new_user_feedback_n'); - late final _value_new_user_feedback_n = - _value_new_user_feedback_nPtr.asFunction< - sentry_value_u Function( - ffi.Pointer, - ffi.Pointer, - int, - ffi.Pointer, - int, - ffi.Pointer, - int)>(); + late final _add_breadcrumbPtr = + _lookup>( + 'sentry_add_breadcrumb'); + late final _add_breadcrumb = + _add_breadcrumbPtr.asFunction(); - /// Captures a manually created User Feedback and sends it to Sentry. - void capture_user_feedback( - sentry_value_u user_feedback, + /// Sets the specified user. + void set_user( + sentry_value_u user, ) { - return _capture_user_feedback( - user_feedback, + return _set_user( + user, ); } - late final _capture_user_feedbackPtr = + late final _set_userPtr = _lookup>( - 'sentry_capture_user_feedback'); - late final _capture_user_feedback = - _capture_user_feedbackPtr.asFunction(); + 'sentry_set_user'); + late final _set_user = + _set_userPtr.asFunction(); - /// Sets a Span's status. - /// - /// The Span should not be mutated by other functions while setting its status. - void span_set_status( - ffi.Pointer span, - int status, + /// Removes a user. + void remove_user() { + return _remove_user(); + } + + late final _remove_userPtr = + _lookup>('sentry_remove_user'); + late final _remove_user = _remove_userPtr.asFunction(); + + /// Sets a tag. + void set_tag( + ffi.Pointer key, + ffi.Pointer value, ) { - return _span_set_status( - span, - status, + return _set_tag( + key, + value, ); } - late final _span_set_statusPtr = _lookup< + late final _set_tagPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int32)>>('sentry_span_set_status'); - late final _span_set_status = _span_set_statusPtr - .asFunction, int)>(); + ffi.Void Function( + ffi.Pointer, ffi.Pointer)>>('sentry_set_tag'); + late final _set_tag = _set_tagPtr.asFunction< + void Function(ffi.Pointer, ffi.Pointer)>(); - /// Sets a Transaction's status. - /// - /// The Transaction should not be mutated by other functions while setting its - /// status. - void transaction_set_status( - ffi.Pointer tx, - int status, + /// Removes the tag with the specified key. + void remove_tag( + ffi.Pointer key, ) { - return _transaction_set_status( - tx, - status, + return _remove_tag( + key, ); } - late final _transaction_set_statusPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Int32)>>('sentry_transaction_set_status'); - late final _transaction_set_status = _transaction_set_statusPtr - .asFunction, int)>(); + late final _remove_tagPtr = + _lookup)>>( + 'sentry_remove_tag'); + late final _remove_tag = + _remove_tagPtr.asFunction)>(); - /// Iterates the distributed tracing HTTP headers for the given span. - void span_iter_headers( - ffi.Pointer span, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>> - callback, - ffi.Pointer userdata, + /// Sets extra information. + void set_extra( + ffi.Pointer key, + sentry_value_u value, ) { - return _span_iter_headers( - span, - callback, - userdata, + return _set_extra( + key, + value, ); } - late final _span_iter_headersPtr = _lookup< + late final _set_extraPtr = _lookup< ffi.NativeFunction< ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>>('sentry_span_iter_headers'); - late final _span_iter_headers = _span_iter_headersPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>(); + ffi.Pointer, sentry_value_u)>>('sentry_set_extra'); + late final _set_extra = _set_extraPtr + .asFunction, sentry_value_u)>(); - /// Iterates the distributed tracing HTTP headers for the given transaction. - void transaction_iter_headers( - ffi.Pointer tx, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, ffi.Pointer, - ffi.Pointer)>> - callback, - ffi.Pointer userdata, + /// Removes the extra with the specified key. + void remove_extra( + ffi.Pointer key, ) { - return _transaction_iter_headers( - tx, - callback, - userdata, + return _remove_extra( + key, ); } - late final _transaction_iter_headersPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>>('sentry_transaction_iter_headers'); - late final _transaction_iter_headers = - _transaction_iter_headersPtr.asFunction< - void Function( - ffi.Pointer, - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ffi.Pointer, ffi.Pointer)>>, - ffi.Pointer)>(); + late final _remove_extraPtr = + _lookup)>>( + 'sentry_remove_extra'); + late final _remove_extra = + _remove_extraPtr.asFunction)>(); - /// Returns whether the application has crashed on the last run. - /// - /// Notes: - /// * The underlying value is set by sentry_init() - it must be called first. - /// * Call sentry_clear_crashed_last_run() to reset for the next app run. - /// - /// Possible return values: - /// 1 = the last run was a crash - /// 0 = no crash recognized - /// -1 = sentry_init() hasn't been called yet - int get_crashed_last_run() { - return _get_crashed_last_run(); + /// Sets a context object. + void set_context( + ffi.Pointer key, + sentry_value_u value, + ) { + return _set_context( + key, + value, + ); } - late final _get_crashed_last_runPtr = - _lookup>( - 'sentry_get_crashed_last_run'); - late final _get_crashed_last_run = - _get_crashed_last_runPtr.asFunction(); + late final _set_contextPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, sentry_value_u)>>('sentry_set_context'); + late final _set_context = _set_contextPtr + .asFunction, sentry_value_u)>(); - /// Clear the status of the "crashed-last-run". You should explicitly call - /// this after sentry_init() if you're using sentry_get_crashed_last_run(). - /// Otherwise, the same information is reported on any subsequent runs. - /// - /// Notes: - /// * This doesn't change the value of sentry_get_crashed_last_run() yet. - /// However, if sentry_init() is called again, the value will change. - /// * This may only be called after sentry_init() and before sentry_close(). - /// - /// Returns 0 on success, 1 on error. - int clear_crashed_last_run() { - return _clear_crashed_last_run(); + /// Removes the context object with the specified key. + void remove_context( + ffi.Pointer key, + ) { + return _remove_context( + key, + ); } - late final _clear_crashed_last_runPtr = - _lookup>( - 'sentry_clear_crashed_last_run'); - late final _clear_crashed_last_run = - _clear_crashed_last_runPtr.asFunction(); + late final _remove_contextPtr = + _lookup)>>( + 'sentry_remove_context'); + late final _remove_context = + _remove_contextPtr.asFunction)>(); /// Sentry SDK version. ffi.Pointer sdk_version() { @@ -4674,18 +820,6 @@ class SentryNative { 'sentry_sdk_name'); late final _sdk_name = _sdk_namePtr.asFunction Function()>(); - - /// Sentry SDK User-Agent set during build time. - /// Deprecated: Please use sentry_options_get_user_agent instead. - ffi.Pointer sdk_user_agent() { - return _sdk_user_agent(); - } - - late final _sdk_user_agentPtr = - _lookup Function()>>( - 'sentry_sdk_user_agent'); - late final _sdk_user_agent = - _sdk_user_agentPtr.asFunction Function()>(); } /// Represents a sentry protocol value. @@ -4721,181 +855,7 @@ abstract class sentry_value_type_t { static const int SENTRY_VALUE_TYPE_OBJECT = 6; } -/// Sentry levels for events and breadcrumbs. -abstract class sentry_level_e { - static const int SENTRY_LEVEL_DEBUG = -1; - static const int SENTRY_LEVEL_INFO = 0; - static const int SENTRY_LEVEL_WARNING = 1; - static const int SENTRY_LEVEL_ERROR = 2; - static const int SENTRY_LEVEL_FATAL = 3; -} - -/// This represents the OS dependent user context in the case of a crash, and can -/// be used to manually capture a crash. -class sentry_ucontext_s extends ffi.Opaque {} - -/// A UUID -class sentry_uuid_s extends ffi.Struct { - @ffi.Array.multi([16]) - external ffi.Array bytes; -} - -/// A Sentry Envelope. -/// -/// The Envelope is an abstract type which represents a payload being sent to -/// sentry. It can contain one or more items, typically an Event. -/// See https://develop.sentry.dev/sdk/envelopes/ -class sentry_envelope_s extends ffi.Opaque {} - -/// This represents an interface for user-defined transports. -/// -/// Transports are responsible for sending envelopes to sentry and are the last -/// step in the event pipeline. -/// -/// Envelopes will be submitted to the transport in a _fire and forget_ fashion, -/// and the transport must send those envelopes _in order_. -/// -/// A transport has the following hooks, all of which -/// take the user provided `state` as last parameter. The transport state needs -/// to be set with `sentry_transport_set_state` and typically holds handles and -/// other information that can be reused across requests. -/// -/// * `send_func`: This function will take ownership of an envelope, and is -/// responsible for freeing it via `sentry_envelope_free`. -/// * `startup_func`: This hook will be called by sentry inside of `sentry_init` -/// and instructs the transport to initialize itself. Failures will bubble up -/// to `sentry_init`. -/// * `flush_func`: Instructs the transport to flush its queue. -/// This hook receives a millisecond-resolution `timeout` parameter and should -/// return `0` if the transport queue is flushed within the timeout. -/// * `shutdown_func`: Instructs the transport to flush its queue and shut down. -/// This hook receives a millisecond-resolution `timeout` parameter and should -/// return `0` if the transport is flushed and shut down successfully. -/// In case of a non-zero return value, sentry will log an error, but continue -/// with freeing the transport. -/// * `free_func`: Frees the transports `state`. This hook might be called even -/// though `shutdown_func` returned a failure code previously. -/// -/// The transport interface might be extended in the future with hooks to flush -/// its internal queue without shutting down, and to dump its internal queue to -/// disk in case of a hard crash. -class sentry_transport_s extends ffi.Opaque {} - /// The Sentry Client Options. /// /// See https://docs.sentry.io/platforms/native/configuration/ class sentry_options_s extends ffi.Opaque {} - -/// This represents an interface for user-defined backends. -/// -/// Backends are responsible to handle crashes. They are maintained at runtime -/// via various life-cycle hooks from the sentry-core. -/// -/// At this point none of those interfaces are exposed in the API including -/// creation and destruction. The main use-case of the backend in the API at this -/// point is to disable it via `sentry_options_set_backend` at runtime before it -/// is initialized. -class sentry_backend_s extends ffi.Opaque {} - -/// The state of user consent. -abstract class sentry_user_consent_t { - static const int SENTRY_USER_CONSENT_UNKNOWN = -1; - static const int SENTRY_USER_CONSENT_GIVEN = 1; - static const int SENTRY_USER_CONSENT_REVOKED = 0; -} - -/// -- Session APIs -- -abstract class sentry_session_status_t { - static const int SENTRY_SESSION_STATUS_OK = 0; - static const int SENTRY_SESSION_STATUS_CRASHED = 1; - static const int SENTRY_SESSION_STATUS_ABNORMAL = 2; - static const int SENTRY_SESSION_STATUS_EXITED = 3; -} - -/// A sentry Transaction Context. -/// -/// See Transaction Interface under -/// https://develop.sentry.dev/sdk/performance/#new-span-and-transaction-classes -class sentry_transaction_context_s extends ffi.Opaque {} - -/// A sentry Transaction. -/// -/// See https://develop.sentry.dev/sdk/event-payloads/transaction/ -class sentry_transaction_s extends ffi.Opaque {} - -/// A sentry Span. -/// -/// See https://develop.sentry.dev/sdk/event-payloads/span/ -class sentry_span_s extends ffi.Opaque {} - -/// The status of a Span or Transaction. -/// -/// See https://develop.sentry.dev/sdk/event-payloads/span/ for documentation. -abstract class sentry_span_status_t { - /// The operation completed successfully. - /// HTTP status 100..299 + successful redirects from the 3xx range. - static const int SENTRY_SPAN_STATUS_OK = 0; - - /// The operation was cancelled (typically by the user). - static const int SENTRY_SPAN_STATUS_CANCELLED = 1; - - /// Unknown. Any non-standard HTTP status code. - /// "We do not know whether the transaction failed or succeeded." - static const int SENTRY_SPAN_STATUS_UNKNOWN = 2; - - /// Client specified an invalid argument. 4xx. - /// Note that this differs from FailedPrecondition. InvalidArgument - /// indicates arguments that are problematic regardless of the - /// state of the system. - static const int SENTRY_SPAN_STATUS_INVALID_ARGUMENT = 3; - - /// Deadline expired before operation could complete. - /// For operations that change the state of the system, this error may be - /// returned even if the operation has been completed successfully. - /// HTTP redirect loops and 504 Gateway Timeout. - static const int SENTRY_SPAN_STATUS_DEADLINE_EXCEEDED = 4; - - /// 404 Not Found. Some requested entity (file or directory) was not found. - static const int SENTRY_SPAN_STATUS_NOT_FOUND = 5; - - /// Already exists (409) - /// Some entity that we attempted to create already exists. - static const int SENTRY_SPAN_STATUS_ALREADY_EXISTS = 6; - - /// 403 Forbidden - /// The caller does not have permission to execute the specified operation. - static const int SENTRY_SPAN_STATUS_PERMISSION_DENIED = 7; - - /// 429 Too Many Requests - /// Some resource has been exhausted, perhaps a per-user quota or perhaps - /// the entire file system is out of space. - static const int SENTRY_SPAN_STATUS_RESOURCE_EXHAUSTED = 8; - - /// Operation was rejected because the system is not in a state required for - /// the operation's execution. - static const int SENTRY_SPAN_STATUS_FAILED_PRECONDITION = 9; - - /// The operation was aborted, typically due to a concurrency issue. - static const int SENTRY_SPAN_STATUS_ABORTED = 10; - - /// Operation was attempted past the valid range. - static const int SENTRY_SPAN_STATUS_OUT_OF_RANGE = 11; - - /// 501 Not Implemented - /// Operation is not implemented or not enabled. - static const int SENTRY_SPAN_STATUS_UNIMPLEMENTED = 12; - - /// Other/generic 5xx - static const int SENTRY_SPAN_STATUS_INTERNAL_ERROR = 13; - - /// 503 Service Unavailable - static const int SENTRY_SPAN_STATUS_UNAVAILABLE = 14; - - /// Unrecoverable data loss or corruption - static const int SENTRY_SPAN_STATUS_DATA_LOSS = 15; - - /// 401 Unauthorized (actually does mean unauthenticated according to RFC - /// 7235) - /// Prefer PermissionDenied if a user is logged in. - static const int SENTRY_SPAN_STATUS_UNAUTHENTICATED = 16; -} From 9a82b38250e960e7a54e33177aef3112627f2558 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 26 Aug 2024 17:49:20 +0200 Subject: [PATCH 11/55] update native crash example --- flutter/example/.gitignore | 3 +++ flutter/example/lib/main.dart | 20 ++++++++------------ flutter/lib/src/native/c/sentry_native.dart | 4 ++++ 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/flutter/example/.gitignore b/flutter/example/.gitignore index c063aec688..856624edb0 100644 --- a/flutter/example/.gitignore +++ b/flutter/example/.gitignore @@ -43,3 +43,6 @@ app.*.map.json # sqflite web/sqflite_sw.js web/sqlite3.wasm + +# Sentry native local storage +.sentry-native diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 7c11d17cca..13a849084b 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -562,6 +562,14 @@ class MainScaffold extends StatelessWidget { if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), + // ignore: invalid_use_of_internal_member + if (SentryFlutter.native != null) + ElevatedButton( + onPressed: () async { + SentryFlutter.nativeCrash(); + }, + child: const Text('Sentry.nativeCrash'), + ), ].map((widget) { if (kIsWeb) { // Add vertical padding to web so the tooltip doesn't obstruct the clicking of the button below. @@ -758,12 +766,6 @@ class AndroidExample extends StatelessWidget { }, child: const Text('Platform exception'), ), - ElevatedButton( - onPressed: () async { - SentryFlutter.nativeCrash(); - }, - child: const Text('Sentry.nativeCrash'), - ), ]); } } @@ -876,12 +878,6 @@ class CocoaExample extends StatelessWidget { }, child: const Text('Objective-C SEGFAULT'), ), - ElevatedButton( - onPressed: () async { - SentryFlutter.nativeCrash(); - }, - child: const Text('Sentry.nativeCrash'), - ), ], ); } diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 82951450b7..81413ed740 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -218,6 +218,10 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr pauseAppHangTracking() {} FutureOr resumeAppHangTracking() {} + + FutureOr nativeCrash() { + Pointer.fromAddress(1).cast().toDartString(); + } } extension on binding.sentry_value_u { From cede7cda646ceb7476d158e7e73f9d9b7635858d Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 6 Sep 2024 09:18:47 +0200 Subject: [PATCH 12/55] post-merge fixup --- flutter/lib/src/native/c/sentry_native.dart | 9 ++++++--- flutter/lib/src/native/sentry_native_binding.dart | 2 +- flutter/test/sentry_native/sentry_native_test.dart | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 81413ed740..aca8cdfb86 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -26,9 +26,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { void _logNotSupported(String operation) => options.logger( SentryLevel.debug, 'SentryNative: $operation is not supported'); - FutureOr init(SentryFlutterOptions options) { - assert(this.options == options); - + FutureOr init(Hub hub) { if (!options.enableNativeCrashHandling) { options.logger( SentryLevel.info, 'SentryNative crash handling is disabled'); @@ -222,6 +220,11 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { FutureOr nativeCrash() { Pointer.fromAddress(1).cast().toDartString(); } + + FutureOr captureReplay(bool isCrash) { + _logNotSupported('capturing replay'); + return SentryId.empty(); + } } extension on binding.sentry_value_u { diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 6ac225e04b..27ae521a2c 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -64,5 +64,5 @@ abstract class SentryNativeBinding { FutureOr nativeCrash(); - Future captureReplay(bool isCrash); + FutureOr captureReplay(bool isCrash); } diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index b00985aea8..f2da4c737c 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -12,6 +12,7 @@ import 'package:sentry_flutter/src/native/c/sentry_native.dart'; import 'package:sentry_flutter/src/native/factory.dart'; import '../mocks.dart'; +import '../mocks.mocks.dart'; void main() { if (Directory.current.path.endsWith('/test')) { @@ -84,7 +85,7 @@ void main() { test('init', () async { // There's nothing we can check here - just that it doesn't crash. - await sut.init(options); + await sut.init(MockHub()); }); test('app start', () { From bdedccfd8dece47ffd40755fdaca9216e66ddac3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Sat, 7 Sep 2024 20:18:05 +0200 Subject: [PATCH 13/55] example: allways show console in the windows sample app --- flutter/example/windows/runner/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flutter/example/windows/runner/main.cpp b/flutter/example/windows/runner/main.cpp index 11ea9c69a7..08f9f5fc8f 100644 --- a/flutter/example/windows/runner/main.cpp +++ b/flutter/example/windows/runner/main.cpp @@ -9,9 +9,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + // if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { CreateAndAttachConsole(); - } + // } // Initialize COM, so that it is available for use in the library and/or // plugins. From 0d07f977858aee1caff21712149b4ae2601ae804 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Sat, 7 Sep 2024 20:18:48 +0200 Subject: [PATCH 14/55] fix: windows build with crashpad --- flutter/pubspec.yaml | 4 +++- flutter/sentry-native/sentry-native.cmake | 18 +++++++++++++++++- flutter/windows/CMakeLists.txt | 10 +++------- .../sentry_flutter/sentry_flutter_plugin.h | 13 +++++++++++++ 4 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 flutter/windows/sentry_flutter/sentry_flutter_plugin.h diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 2b3f7d71e0..d8708ff3e8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -61,4 +61,6 @@ flutter: linux: pluginClass: SentryFlutterPlugin windows: - ffiPlugin: true + # Note, we cannot use `ffiPlugin: true` because flutter tooling won't add `target_link_libraries()` + # so sentry-native won't even build during the build process (since it doesn't need to). + pluginClass: SentryFlutterPlugin diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake index 1882509324..dac1ead7d5 100644 --- a/flutter/sentry-native/sentry-native.cmake +++ b/flutter/sentry-native/sentry-native.cmake @@ -1,8 +1,9 @@ +cmake_minimum_required(VERSION 3.28) load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo version) message(STATUS "Fetching Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") set(SENTRY_SDK_NAME "sentry.native.flutter" CACHE STRING "The SDK name to report when sending events." FORCE) -set(SENTRY_BACKEND "inproc" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) +set(SENTRY_BACKEND "crashpad" CACHE STRING "The sentry backend responsible for reporting crashes" FORCE) set(SENTRY_BUILD_SHARED_LIBS ON CACHE BOOL "Build shared libraries (.dll/.so) instead of static ones (.lib/.a)" FORCE) include(FetchContent) @@ -10,6 +11,21 @@ FetchContent_Declare( sentry-native GIT_REPOSITORY ${SENTRY_NATIVE_repo} GIT_TAG ${SENTRY_NATIVE_version} + EXCLUDE_FROM_ALL ) FetchContent_MakeAvailable(sentry-native) + +# List of absolute paths to libraries that should be bundled with the plugin. +# This list could contain prebuilt libraries, or libraries created by an +# external build triggered from this build file. +set(sentry_flutter_bundled_libraries + $ + PARENT_SCOPE +) + +# `*_plugin` is the name of the plugin library as expected by flutter. +# We don't actually need a plugin here, we just need to get the native library linked +# The following generated code achieves that: +# https://github.com/flutter/flutter/blob/ebfaa45c7d23374a7f3f596adea62ae1dd4e5845/packages/flutter_tools/lib/src/flutter_plugins.dart#L591-L596 +add_library(sentry_flutter_plugin ALIAS sentry) diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 68e4e45e64..f992a75fdb 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -6,10 +6,6 @@ cmake_minimum_required(VERSION 3.14) include("${CMAKE_CURRENT_SOURCE_DIR}/../sentry-native/sentry-native.cmake") -# List of absolute paths to libraries that should be bundled with the plugin. -# This list could contain prebuilt libraries, or libraries created by an -# external build triggered from this build file. -set(sentry_flutter_bundled_libraries - $ - PARENT_SCOPE -) +# Even though sentry_flutter doesn't actually provide a useful plugin, we need to accomodate the Flutter tooling. +# sentry_flutter/sentry_flutter_plugin.h is included by the flutter-tool generated plugin registrar: +target_include_directories(sentry INTERFACE ${CMAKE_CURRENT_LIST_DIR}) diff --git a/flutter/windows/sentry_flutter/sentry_flutter_plugin.h b/flutter/windows/sentry_flutter/sentry_flutter_plugin.h new file mode 100644 index 0000000000..328a651d12 --- /dev/null +++ b/flutter/windows/sentry_flutter/sentry_flutter_plugin.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#if defined(__cplusplus) +extern "C" { +#endif + +void SentryFlutterPluginRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {} + +#if defined(__cplusplus) +} // extern "C" +#endif From 0ff707b835456e3e1b2c666f7a979824d2fa0f39 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Sat, 7 Sep 2024 20:59:24 +0200 Subject: [PATCH 15/55] update test script --- flutter/example/run.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter/example/run.sh b/flutter/example/run.sh index 025a6f31b4..b329d61be9 100755 --- a/flutter/example/run.sh +++ b/flutter/example/run.sh @@ -35,6 +35,9 @@ elif [ "$1" == "web" ]; then elif [ "$1" == "macos" ]; then flutter build macos --split-debug-info=$symbolsDir --obfuscate launchCmd='./build/macos/Build/Products/Release/sentry_flutter_example.app/Contents/MacOS/sentry_flutter_example' +elif [ "$1" == "windows" ]; then + flutter build windows --split-debug-info=$symbolsDir --obfuscate + launchCmd='./build/windows/x64/runner/Release/sentry_flutter_example.exe' else if [ "$1" == "" ]; then echo -e "[\033[92mrun\033[0m] Pass the platform you'd like to run: android, ios, web" From e258718aa21cdba4703219011c0e106909b76b54 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 9 Sep 2024 16:29:19 +0200 Subject: [PATCH 16/55] tmp --- dart/lib/src/sentry_exception_factory.dart | 2 ++ .../load_image_list_integration.dart | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index 9ee2148c14..a188178c5a 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -61,6 +61,8 @@ class SentryExceptionFactory { final stackTraceString = stackTrace.toString(); final value = throwableString.replaceAll(stackTraceString, '').trim(); + print(stackTraceString); + String errorTypeName = throwable.runtimeType.toString(); if (_options.enableExceptionTypeIdentification) { diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index a3a1c9fc9d..742f05610b 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -56,6 +56,26 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { if (event.needsSymbolication()) { final images = await _native.loadDebugImages(); if (images != null) { + try { + final frames = event._getStacktraceFrames()!; + for (var frame in frames) { + print("Checking frame: ${frame?.instructionAddr}"); + final frameAddr = int.parse(frame!.instructionAddr!); + print("frameAddr: $frameAddr"); + for (var image in images) { + final imageStart = int.parse(image.imageAddr!); + final imageEnd = imageStart + image.imageSize!; + print( + " Checking against image: ${image.name}: $imageStart - $imageEnd"); + if (frameAddr >= imageStart && frameAddr < imageEnd) { + print(" Found frame to match image: ${image.name}"); + break; + } + } + } + } catch (e) { + print("Error: $e"); + } return event.copyWith(debugMeta: DebugMeta(images: images)); } } From 8fc78fd33ffe791ae1999e74ace1bac01e63b90c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 9 Sep 2024 20:10:52 +0200 Subject: [PATCH 17/55] tmp --- .../lib/src/integrations/load_image_list_integration.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 742f05610b..9e64c50d2f 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -65,10 +65,9 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { for (var image in images) { final imageStart = int.parse(image.imageAddr!); final imageEnd = imageStart + image.imageSize!; - print( - " Checking against image: ${image.name}: $imageStart - $imageEnd"); if (frameAddr >= imageStart && frameAddr < imageEnd) { - print(" Found frame to match image: ${image.name}"); + print( + " Found image: $imageStart - $imageEnd: ${image.toJson()}"); break; } } From 6cb6c2b683f642b0b0b74ccf33949ebe642c4631 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 9 Sep 2024 20:44:08 +0200 Subject: [PATCH 18/55] tmp --- .../lib/src/integrations/load_image_list_integration.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 9e64c50d2f..956e888114 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -72,6 +72,13 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { } } } + print("--------------------"); + for (var image in images) { + final imageStart = int.parse(image.imageAddr!); + final imageEnd = imageStart + image.imageSize!; + print("image: $imageStart - $imageEnd: ${image.toJson()}"); + } + print("--------------------"); } catch (e) { print("Error: $e"); } From 4e9da9eba6cc9921ea33da9c53401c811a4bf179 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 9 Sep 2024 21:36:20 +0200 Subject: [PATCH 19/55] remove cmake_minimum_required --- flutter/sentry-native/sentry-native.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake index dac1ead7d5..346f4b1457 100644 --- a/flutter/sentry-native/sentry-native.cmake +++ b/flutter/sentry-native/sentry-native.cmake @@ -1,4 +1,3 @@ -cmake_minimum_required(VERSION 3.28) load_cache("${CMAKE_CURRENT_LIST_DIR}" READ_WITH_PREFIX SENTRY_NATIVE_ repo version) message(STATUS "Fetching Sentry native version: ${SENTRY_NATIVE_version} from ${SENTRY_NATIVE_repo}") From 00692fc5d645845665071bd3363e5ade0c3cbfda Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 11 Sep 2024 20:14:06 +0200 Subject: [PATCH 20/55] windows native obfuscated stack trace support --- dart/lib/src/protocol/sentry_stack_trace.dart | 10 ++ dart/lib/src/sentry_client.dart | 7 +- dart/lib/src/sentry_exception_factory.dart | 13 +- dart/lib/src/sentry_stack_trace_factory.dart | 73 ++++++++--- dart/test/mocks/mock_platform.dart | 4 + dart/test/sentry_client_test.dart | 6 +- dart/test/sentry_exception_factory_test.dart | 40 +++--- dart/test/stack_trace_test.dart | 27 ++-- .../load_image_list_integration.dart | 120 ++++++++++-------- .../integrations/load_image_list_test.dart | 28 +++- 10 files changed, 203 insertions(+), 125 deletions(-) diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index 949318ec4c..8f229462dd 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -12,6 +12,8 @@ class SentryStackTrace { this.lang, this.snapshot, this.unknown, + @internal this.nativeImageBaseAddr, + @internal this.nativeBuildId, }) : _frames = frames, _registers = Map.from(registers ?? {}); @@ -46,6 +48,12 @@ class SentryStackTrace { /// signal. final bool? snapshot; + @internal + final String? nativeImageBaseAddr; + + @internal + final String? nativeBuildId; + @internal final Map? unknown; @@ -91,5 +99,7 @@ class SentryStackTrace { lang: lang ?? this.lang, snapshot: snapshot ?? this.snapshot, unknown: unknown, + nativeImageBaseAddr: nativeImageBaseAddr, + nativeBuildId: nativeBuildId, ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index b66c0d25b5..3675b66603 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -269,9 +269,8 @@ class SentryClient { // https://develop.sentry.dev/sdk/event-payloads/stacktrace/ if (stackTrace != null || _options.attachStacktrace) { stackTrace ??= getCurrentStackTrace(); - final frames = _stackTraceFactory.getStackFrames(stackTrace); - - if (frames.isNotEmpty) { + final sentryStackTrace = _stackTraceFactory.create(stackTrace); + if (sentryStackTrace.frames.isNotEmpty) { event = event.copyWith(threads: [ ...?event.threads, SentryThread( @@ -279,7 +278,7 @@ class SentryClient { id: isolateId, crashed: false, current: true, - stacktrace: SentryStackTrace(frames: frames), + stacktrace: sentryStackTrace, ), ]); } diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index a188178c5a..e019b45e94 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -47,13 +47,10 @@ class SentryExceptionFactory { SentryStackTrace? sentryStackTrace; if (stackTrace != null) { - final frames = _stacktraceFactory.getStackFrames(stackTrace); - - if (frames.isNotEmpty) { - sentryStackTrace = SentryStackTrace( - frames: frames, - snapshot: snapshot, - ); + sentryStackTrace = + _stacktraceFactory.create(stackTrace).copyWith(snapshot: snapshot); + if (sentryStackTrace.frames.isEmpty) { + sentryStackTrace = null; } } @@ -61,8 +58,6 @@ class SentryExceptionFactory { final stackTraceString = stackTrace.toString(); final value = throwableString.replaceAll(stackTraceString, '').trim(); - print(stackTraceString); - String errorTypeName = throwable.runtimeType.toString(); if (_options.enableExceptionTypeIdentification) { diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index 7aab228a3e..994e4be479 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -9,22 +9,28 @@ import 'sentry_options.dart'; class SentryStackTraceFactory { final SentryOptions _options; - final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); - final _frameRegex = RegExp(r'^\s*#', multiLine: true); - + static final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); + static final _frameRegex = RegExp(r'^\s*#', multiLine: true); + static final _buildIdRegex = RegExp(r"build_id: '([A-Fa-f0-9]+)'"); + static final _baseAddrRegex = RegExp(r'vm_dso_base: ([A-Fa-f0-9]+)'); static final SentryStackFrame _asynchronousGapFrameJson = SentryStackFrame(absPath: ''); SentryStackTraceFactory(this._options); /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) + @deprecated List getStackFrames(dynamic stackTrace) { - final chain = _parseStackTrace(stackTrace); + return create(stackTrace).frames; + } + + SentryStackTrace create(dynamic stackTrace) { + final parsed = _parseStackTrace(stackTrace); final frames = []; var onlyAsyncGap = true; - for (var t = 0; t < chain.traces.length; t += 1) { - final trace = chain.traces[t]; + for (var t = 0; t < parsed.traces.length; t += 1) { + final trace = parsed.traces[t]; // NOTE: We want to keep the Sentry frames for crash detection // this does not affect grouping since they're not marked as inApp @@ -37,17 +43,23 @@ class SentryStackTraceFactory { } // fill asynchronous gap - if (t < chain.traces.length - 1) { + if (t < parsed.traces.length - 1) { frames.add(_asynchronousGapFrameJson); } } - return onlyAsyncGap ? [] : frames.reversed.toList(); + return SentryStackTrace( + frames: onlyAsyncGap ? [] : frames.reversed.toList(), + nativeImageBaseAddr: parsed.imageBaseAddr, + nativeBuildId: parsed.buildId, + ); } - Chain _parseStackTrace(dynamic stackTrace) { - if (stackTrace is Chain || stackTrace is Trace) { - return Chain.forTrace(stackTrace); + _StackInfo _parseStackTrace(dynamic stackTrace) { + if (stackTrace is Chain) { + return _StackInfo(stackTrace.traces); + } else if (stackTrace is Trace) { + return _StackInfo([stackTrace]); } // We need to convert to string and split the headers manually, otherwise @@ -62,16 +74,34 @@ class SentryStackTraceFactory { // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** // pid: 19226, tid: 6103134208, name io.flutter.ui // os: macos arch: arm64 comp: no sim: no + // build_id: 'bca64abfdfcc84d231bb8f1ccdbfbd8d' // isolate_dso_base: 10fa20000, vm_dso_base: 10fa20000 // isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 // #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 // #01 abs 000000723d637527 _kDartIsolateSnapshotInstructions+0x1e5527 final startOffset = _frameRegex.firstMatch(stackTrace)?.start ?? 0; - return Chain.parse( + final chain = Chain.parse( startOffset == 0 ? stackTrace : stackTrace.substring(startOffset)); + final info = _StackInfo(chain.traces); + + // On Windows native, we need to provide the debug image address so that + // the LoadImageList integration can add a virtual image. + // This is because on Windows, dart uses ELF binaries for AOT code instead + // of standard windows-specific PE and it has a custom ELF image loader + // that loaads the "data/App.so". Therefore, sentry-native is unaware + // of this and won't pick list the image among native images. + // See https://github.com/flutter/flutter/issues/154840 + if (_options.platformChecker.platform.isWindows) { + info.buildId = _buildIdRegex.firstMatch(stackTrace)?.group(1); + info.imageBaseAddr = _baseAddrRegex.firstMatch(stackTrace)?.group(1); + if (info.imageBaseAddr != null) { + info.imageBaseAddr = '0x${info.imageBaseAddr}'; + } + } + return info; } - return Chain([]); + return _StackInfo([]); } /// converts [Frame] to [SentryStackFrame] @@ -81,20 +111,15 @@ class SentryStackTraceFactory { if (frame is UnparsedFrame && member != null) { // if --split-debug-info is enabled, thats what we see: - // *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** - // pid: 19226, tid: 6103134208, name io.flutter.ui - // os: macos arch: arm64 comp: no sim: no - // isolate_dso_base: 10fa20000, vm_dso_base: 10fa20000 - // isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 // #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 - // #01 abs 000000723d637527 _kDartIsolateSnapshotInstructions+0x1e5527 // we are only interested on the #01, 02... items which contains the 'abs' addresses. final match = _absRegex.firstMatch(member); if (match != null) { return SentryStackFrame( instructionAddr: '0x${match.group(1)!}', - platform: 'native', // to trigger symbolication & native LoadImageList + // 'native' triggers the [LoadImageListIntegration] and server-side symbolication + platform: 'native', ); } @@ -182,3 +207,11 @@ class SentryStackTraceFactory { return _options.considerInAppFramesByDefault; } } + +class _StackInfo { + String? imageBaseAddr; + String? buildId; + final List traces; + + _StackInfo(this.traces); +} diff --git a/dart/test/mocks/mock_platform.dart b/dart/test/mocks/mock_platform.dart index a045f794af..32c080385b 100644 --- a/dart/test/mocks/mock_platform.dart +++ b/dart/test/mocks/mock_platform.dart @@ -13,6 +13,10 @@ class MockPlatform extends Platform with NoSuchMethodProvider { return MockPlatform(os: 'ios'); } + factory MockPlatform.windows() { + return MockPlatform(os: 'windows'); + } + @override String operatingSystem; } diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 07d5aab834..4efe53a01d 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -97,10 +97,8 @@ void main() { final exception = SentryException( type: 'Exception', value: 'an exception', - stackTrace: SentryStackTrace( - frames: SentryStackTraceFactory(fixture.options) - .getStackFrames('#0 baz (file:///pathto/test.dart:50:3)'), - ), + stackTrace: SentryStackTraceFactory(fixture.options) + .create('#0 baz (file:///pathto/test.dart:50:3)'), ); final event = SentryEvent(exceptions: [exception]); diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index cca350f2b4..73b412da3f 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -3,9 +3,14 @@ import 'package:sentry/src/sentry_exception_factory.dart'; import 'package:test/test.dart'; import 'mocks.dart'; +import 'mocks/mock_platform.dart'; +import 'mocks/mock_platform_checker.dart'; void main() { - final fixture = Fixture(); + late Fixture fixture; + setUp(() { + fixture = Fixture(); + }); test('getSentryException with frames', () { SentryException sentryException; @@ -206,6 +211,18 @@ void main() { expect(sentryException.stackTrace!.snapshot, true); }); + + test('sets native image start address on Windows', () { + fixture.options.platformChecker = + MockPlatformChecker(platform: MockPlatform.windows()); + final sentryException = fixture + .getSut(attachStacktrace: false) + .getSentryException(Object(), stackTrace: StackTraceErrorStackTrace()); + + final sentryStackTrace = sentryException.stackTrace!; + expect(sentryStackTrace.nativeImageBaseAddr, '0x752602b000'); + expect(sentryStackTrace.nativeBuildId, 'bca64abfdfcc84d231bb8f1ccdbfbd8d'); + }); } class CustomError extends Error {} @@ -233,26 +250,7 @@ class StackTraceError extends Error { return ''' $prefix -pid: 9437, tid: 10069, name 1.ui -os: android arch: arm64 comp: yes sim: no -build_id: 'bca64abfdfcc84d231bb8f1ccdbfbd8d' -isolate_dso_base: 752602b000, vm_dso_base: 752602b000 -isolate_instructions: 7526344980, vm_instructions: 752633f000 -#00 abs 00000075266c2fbf virt 0000000000697fbf _kDartIsolateSnapshotInstructions+0x37e63f -#1 abs 000000752685211f virt 000000000082711f _kDartIsolateSnapshotInstructions+0x50d79f -#2 abs 0000007526851cb3 virt 0000000000826cb3 _kDartIsolateSnapshotInstructions+0x50d333 -#3 abs 0000007526851c63 virt 0000000000826c63 _kDartIsolateSnapshotInstructions+0x50d2e3 -#4 abs 0000007526851bf3 virt 0000000000826bf3 _kDartIsolateSnapshotInstructions+0x50d273 -#5 abs 0000007526a0b44b virt 00000000009e044b _kDartIsolateSnapshotInstructions+0x6c6acb -#6 abs 0000007526a068a7 virt 00000000009db8a7 _kDartIsolateSnapshotInstructions+0x6c1f27 -#7 abs 0000007526b57a2b virt 0000000000b2ca2b _kDartIsolateSnapshotInstructions+0x8130ab -#8 abs 0000007526b5d93b virt 0000000000b3293b _kDartIsolateSnapshotInstructions+0x818fbb -#9 abs 0000007526a2333b virt 00000000009f833b _kDartIsolateSnapshotInstructions+0x6de9bb -#10 abs 0000007526937957 virt 000000000090c957 _kDartIsolateSnapshotInstructions+0x5f2fd7 -#11 abs 0000007526a243a3 virt 00000000009f93a3 _kDartIsolateSnapshotInstructions+0x6dfa23 -#12 abs 000000752636273b virt 000000000033773b _kDartIsolateSnapshotInstructions+0x1ddbb -#13 abs 0000007526a36ac3 virt 0000000000a0bac3 _kDartIsolateSnapshotInstructions+0x6f2143 -#14 abs 00000075263626af virt 00000000003376af _kDartIsolateSnapshotInstructions+0x1dd2f'''; +${StackTraceErrorStackTrace()}'''; } } diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index 3636d845a4..5a0dde71d6 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -111,12 +111,11 @@ void main() { group('encodeStackTrace', () { test('encodes a simple stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).create(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { @@ -141,13 +140,12 @@ void main() { }); test('encodes an asynchronous stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).create(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { @@ -202,7 +200,8 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 for (var traceString in stackTraces) { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .getStackFrames(traceString) + .create(traceString) + .frames .map((frame) => frame.toJson()); expect( @@ -222,13 +221,12 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 }); test('parses normal stack trace', () { - final frames = Fixture() - .getSut(considerInAppFramesByDefault: true) - .getStackFrames(''' + final frames = + Fixture().getSut(considerInAppFramesByDefault: true).create(''' #0 asyncThrows (file:/foo/bar/main.dart:404) #1 MainScaffold.build. (package:example/main.dart:131) #2 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:341) - ''').map((frame) => frame.toJson()); + ''').frames.map((frame) => frame.toJson()); expect(frames, [ { 'filename': 'platform_dispatcher.dart', @@ -261,9 +259,10 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 test('remove frames if only async gap is left', () { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .getStackFrames(StackTrace.fromString(''' + .create(StackTrace.fromString(''' ''')) + .frames .map((frame) => frame.toJson()); expect(frames.isEmpty, true); }); diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 956e888114..abc47f9cf0 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -1,6 +1,10 @@ import 'dart:async'; +import 'package:file/local.dart'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; +import 'package:file/file.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; @@ -13,74 +17,49 @@ class LoadImageListIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { options.addEventProcessor( - _LoadImageListIntegrationEventProcessor(_native), + _LoadImageListIntegrationEventProcessor(options, _native), ); options.sdk.addIntegration('loadImageListIntegration'); } } -extension _NeedsSymbolication on SentryEvent { - bool needsSymbolication() { - if (this is SentryTransaction) { - return false; - } - final frames = _getStacktraceFrames(); - if (frames == null) { - return false; - } - return frames.any((frame) => 'native' == frame?.platform); - } - - Iterable? _getStacktraceFrames() { - if (exceptions?.isNotEmpty == true) { - return exceptions?.first.stackTrace?.frames; - } - if (threads?.isNotEmpty == true) { - var stacktraces = threads?.map((e) => e.stacktrace); - return stacktraces - ?.where((element) => element != null) - .expand((element) => element!.frames); - } - return null; +extension on SentryEvent { + SentryStackTrace? _getStacktrace() { + var stackTrace = + exceptions?.firstWhereOrNull((e) => e.stackTrace != null)?.stackTrace; + stackTrace ??= + threads?.firstWhereOrNull((t) => t.stacktrace != null)?.stacktrace; + return stackTrace; } } class _LoadImageListIntegrationEventProcessor implements EventProcessor { - _LoadImageListIntegrationEventProcessor(this._native); + _LoadImageListIntegrationEventProcessor(this._options, this._native); + final SentryFlutterOptions _options; final SentryNativeBinding _native; + DebugImage? _appDebugImage; + + @visibleForTesting + FileSystem fs = LocalFileSystem(); @override Future apply(SentryEvent event, Hint hint) async { - if (event.needsSymbolication()) { + final stackTrace = event._getStacktrace(); + + // if the stacktrace has native frames, we load native debug images. + if (stackTrace != null && + stackTrace.frames.any((frame) => 'native' == frame.platform)) { final images = await _native.loadDebugImages(); if (images != null) { - try { - final frames = event._getStacktraceFrames()!; - for (var frame in frames) { - print("Checking frame: ${frame?.instructionAddr}"); - final frameAddr = int.parse(frame!.instructionAddr!); - print("frameAddr: $frameAddr"); - for (var image in images) { - final imageStart = int.parse(image.imageAddr!); - final imageEnd = imageStart + image.imageSize!; - if (frameAddr >= imageStart && frameAddr < imageEnd) { - print( - " Found image: $imageStart - $imageEnd: ${image.toJson()}"); - break; - } - } + // On windows, we need to add the ELF debug image of the AOT code. + // See https://github.com/flutter/flutter/issues/154840 + if (_options.platformChecker.platform.isWindows) { + _appDebugImage ??= await _getAppDebugImage(stackTrace, images); + if (_appDebugImage != null) { + images.add(_appDebugImage!); } - print("--------------------"); - for (var image in images) { - final imageStart = int.parse(image.imageAddr!); - final imageEnd = imageStart + image.imageSize!; - print("image: $imageStart - $imageEnd: ${image.toJson()}"); - } - print("--------------------"); - } catch (e) { - print("Error: $e"); } return event.copyWith(debugMeta: DebugMeta(images: images)); } @@ -88,4 +67,45 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { return event; } + + Future _getAppDebugImage( + SentryStackTrace stackTrace, Iterable nativeImages) async { + // ignore: invalid_use_of_internal_member + final buildId = stackTrace.nativeBuildId; + // ignore: invalid_use_of_internal_member + final imageAddr = stackTrace.nativeImageBaseAddr; + + if (buildId == null || imageAddr == null) { + return null; + } + + final exePath = nativeImages + .firstWhereOrNull( + (image) => image.codeFile?.toLowerCase().endsWith('.exe') ?? false) + ?.codeFile; + if (exePath == null) { + _options.logger( + SentryLevel.debug, + "Couldn't add AOT ELF image for server-side symbolication because the " + "app executable is not among the debug images reported by native."); + return null; + } + + final appSoFile = + fs.file(exePath).parent.childDirectory('data').childFile('app.so'); + if (!await appSoFile.exists()) { + _options.logger(SentryLevel.debug, + "Couldn't add AOT ELF image because ${appSoFile.path} doesn't exist."); + return null; + } + + final stat = await appSoFile.stat(); + return DebugImage( + type: 'elf', + imageAddr: imageAddr, + imageSize: stat.size, + codeFile: appSoFile.path, + codeId: buildId, + ); + } } diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 35e59b7599..607bf8ba33 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -1,6 +1,7 @@ @TestOn('vm') library flutter_test; +import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -12,7 +13,7 @@ void main() { group(LoadImageListIntegration, () { final imageList = [ DebugImage.fromJson({ - 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', + 'code_file': '/path/to/app.exe', 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', 'image_addr': '0x6f80b000', 'image_size': 3092480, @@ -87,7 +88,7 @@ void main() { final image = event!.debugMeta!.images.first; - expect('/apex/com.android.art/javalib/arm64/boot.oat', image.codeFile); + expect('/path/to/app.exe', image.codeFile); expect('13577ce71153c228ecf0eb73fc39f45010d487f8', image.codeId); expect('0x6f80b000', image.imageAddr); expect(3092480, image.imageSize); @@ -102,11 +103,32 @@ void main() { await fixture.hub.captureMessage('error'); verifyNever(fixture.binding.loadDebugImages()); }); + + test('ELF image is added on Windows for obfuscated stack trace', () async { + final ep = fixture.options.eventProcessors.first; + + final fs = MemoryFileSystem.test(); + (ep as dynamic).fs = fs; + await fs.directory('/path/to/data').create(recursive: true); + await fs.file('/path/to/data/app.so').writeAsString('12345'); + + final event = await ep.apply(_getEvent(), Hint()); + + final images = event!.debugMeta!.images; + expect(images.length, 2); + + final image = images.last; + expect(image.codeFile, '/path/to/data/app.so'); + expect(image.codeId, 'foo'); + expect(image.imageAddr, '0x123'); + expect(image.imageSize, 5); + }); }); } SentryEvent _getEvent() { final frame = SentryStackFrame(platform: 'native'); - final st = SentryStackTrace(frames: [frame]); + final st = SentryStackTrace( + frames: [frame], nativeImageBaseAddr: '0x123', nativeBuildId: 'foo'); return SentryEvent(threads: [SentryThread(stacktrace: st)]); } From f70dbbba68439386707d0fd7944502d3aa6a2bec Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 13:47:33 +0200 Subject: [PATCH 21/55] move native debug image resolution to sentry_native.dart --- .../load_image_list_integration.dart | 63 +--- flutter/lib/src/native/c/sentry_native.dart | 97 ++++- .../lib/src/native/sentry_native_binding.dart | 2 +- .../lib/src/native/sentry_native_channel.dart | 2 +- .../integrations/load_image_list_test.dart | 29 +- flutter/test/mocks.mocks.dart | 344 +++++++----------- .../sentry_native/sentry_native_test.dart | 33 +- flutter/test/sentry_native_channel_test.dart | 2 +- 8 files changed, 256 insertions(+), 316 deletions(-) diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index abc47f9cf0..aacc75de16 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -1,10 +1,7 @@ import 'dart:async'; -import 'package:file/local.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; -import 'package:file/file.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; @@ -17,7 +14,7 @@ class LoadImageListIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { options.addEventProcessor( - _LoadImageListIntegrationEventProcessor(options, _native), + _LoadImageListIntegrationEventProcessor(_native), ); options.sdk.addIntegration('loadImageListIntegration'); @@ -35,14 +32,9 @@ extension on SentryEvent { } class _LoadImageListIntegrationEventProcessor implements EventProcessor { - _LoadImageListIntegrationEventProcessor(this._options, this._native); + _LoadImageListIntegrationEventProcessor(this._native); - final SentryFlutterOptions _options; final SentryNativeBinding _native; - DebugImage? _appDebugImage; - - @visibleForTesting - FileSystem fs = LocalFileSystem(); @override Future apply(SentryEvent event, Hint hint) async { @@ -51,61 +43,12 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { // if the stacktrace has native frames, we load native debug images. if (stackTrace != null && stackTrace.frames.any((frame) => 'native' == frame.platform)) { - final images = await _native.loadDebugImages(); + final images = await _native.loadDebugImages(stackTrace); if (images != null) { - // On windows, we need to add the ELF debug image of the AOT code. - // See https://github.com/flutter/flutter/issues/154840 - if (_options.platformChecker.platform.isWindows) { - _appDebugImage ??= await _getAppDebugImage(stackTrace, images); - if (_appDebugImage != null) { - images.add(_appDebugImage!); - } - } return event.copyWith(debugMeta: DebugMeta(images: images)); } } return event; } - - Future _getAppDebugImage( - SentryStackTrace stackTrace, Iterable nativeImages) async { - // ignore: invalid_use_of_internal_member - final buildId = stackTrace.nativeBuildId; - // ignore: invalid_use_of_internal_member - final imageAddr = stackTrace.nativeImageBaseAddr; - - if (buildId == null || imageAddr == null) { - return null; - } - - final exePath = nativeImages - .firstWhereOrNull( - (image) => image.codeFile?.toLowerCase().endsWith('.exe') ?? false) - ?.codeFile; - if (exePath == null) { - _options.logger( - SentryLevel.debug, - "Couldn't add AOT ELF image for server-side symbolication because the " - "app executable is not among the debug images reported by native."); - return null; - } - - final appSoFile = - fs.file(exePath).parent.childDirectory('data').childFile('app.so'); - if (!await appSoFile.exists()) { - _options.logger(SentryLevel.debug, - "Couldn't add AOT ELF image because ${appSoFile.path} doesn't exist."); - return null; - } - - final stat = await appSoFile.stat(); - return DebugImage( - type: 'elf', - imageAddr: imageAddr, - imageSize: stat.size, - codeFile: appSoFile.path, - codeId: buildId, - ); - } } diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index aca8cdfb86..1b028c378e 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; @@ -15,6 +16,7 @@ import 'utils.dart'; @internal class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { + DebugImage? _appDebugImage; final SentryFlutterOptions options; @visibleForTesting @@ -186,8 +188,8 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return null; } - FutureOr?> loadDebugImages() => - tryCatchSync('get_module_list', () { + FutureOr?> loadDebugImages(SentryStackTrace stackTrace) => + tryCatchAsync('get_module_list', () async { final cImages = native.get_modules_list(); try { if (native.value_get_type(cImages) != @@ -195,8 +197,8 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return null; } - return List.generate(native.value_get_length(cImages), - (index) { + final images = List.generate( + native.value_get_length(cImages), (index) { final cImage = native.value_get_by_index(cImages, index); return DebugImage( type: cImage.get('type').castPrimitive(options.logger) ?? '', @@ -208,11 +210,82 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { codeId: cImage.get('code_id').castPrimitive(options.logger), ); }); + + // On windows, we need to add the ELF debug image of the AOT code. + // See https://github.com/flutter/flutter/issues/154840 + if (options.platformChecker.platform.isWindows) { + _appDebugImage ??= await _getAppDebugImage(stackTrace, images); + if (_appDebugImage != null) { + images.add(_appDebugImage!); + } + } + + return images; } finally { native.value_decref(cImages); } }); + Future _getAppDebugImage( + SentryStackTrace stackTrace, Iterable nativeImages) async { + // ignore: invalid_use_of_internal_member + final buildId = stackTrace.nativeBuildId; + // ignore: invalid_use_of_internal_member + final imageAddr = stackTrace.nativeImageBaseAddr; + + if (buildId == null || imageAddr == null) { + return null; + } + + final exePath = nativeImages + .firstWhereOrNull( + (image) => image.codeFile?.toLowerCase().endsWith('.exe') ?? false) + ?.codeFile; + if (exePath == null) { + options.logger( + SentryLevel.debug, + "Couldn't add AOT ELF image for server-side symbolication because the " + "app executable is not among the debug images reported by native."); + return null; + } + + final appSoFile = options.fileSystem + .file(exePath) + .parent + .childDirectory('data') + .childFile('app.so'); + if (!await appSoFile.exists()) { + options.logger(SentryLevel.debug, + "Couldn't add AOT ELF image because ${appSoFile.path} doesn't exist."); + return null; + } + + // Symbolicator requires Debug ID to find the debug image. We can construct + // it from the buildID: https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 + // For example + // Build ID: 4c6950bd9e9cc9839071742a7295c09e + // Debug ID: bd50694c-9c9e-83c9-9071-742a7295c09e + String? debugId; + if (buildId.length == 16) { + final p = _DwarfDebugIdBuilder(buildId); + debugId = + "${p.take(4)}-${p.take(2)}-${p.take(2)}-${p.take(2, false)}-${p.take(6, false)}"; + } else { + options.logger(SentryLevel.debug, + "Couldn't construct AOT ELF image DebugID because ${appSoFile.path} doesn't exist."); + } + + final stat = await appSoFile.stat(); + return DebugImage( + type: 'elf', + imageAddr: imageAddr, + imageSize: stat.size, + codeFile: appSoFile.path, + codeId: buildId, + debugId: debugId, + ); + } + FutureOr pauseAppHangTracking() {} FutureOr resumeAppHangTracking() {} @@ -349,3 +422,19 @@ extension on List { return cObject; } } + +// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 +class _DwarfDebugIdBuilder { + Iterable> _chunks; + + _DwarfDebugIdBuilder(String buildId) : _chunks = buildId.split('').slices(2); + + String take(int numChunks, [bool reversed = true]) { + var part = _chunks.take(numChunks); + _chunks = _chunks.skip(numChunks); + if (reversed) { + part = part.toList().reversed; + } + return part.map((chunk) => chunk.join()).join(); + } +} diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 27ae521a2c..00fa6c4104 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -56,7 +56,7 @@ abstract class SentryNativeBinding { FutureOr?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs); - FutureOr?> loadDebugImages(); + FutureOr?> loadDebugImages(SentryStackTrace stackTrace); FutureOr pauseAppHangTracking(); diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index 40e712e82b..2d17ed583f 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -180,7 +180,7 @@ class SentryNativeChannel }); @override - Future?> loadDebugImages() => + Future?> loadDebugImages(SentryStackTrace stackTrace) => tryCatchAsync('loadDebugImages', () async { final images = await channel .invokeListMethod>('loadImageList'); diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 607bf8ba33..5015990985 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -1,7 +1,6 @@ @TestOn('vm') library flutter_test; -import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -13,7 +12,7 @@ void main() { group(LoadImageListIntegration, () { final imageList = [ DebugImage.fromJson({ - 'code_file': '/path/to/app.exe', + 'code_file': '/apex/com.android.art/javalib/arm64/boot.oat', 'code_id': '13577ce71153c228ecf0eb73fc39f45010d487f8', 'image_addr': '0x6f80b000', 'image_size': 3092480, @@ -88,7 +87,7 @@ void main() { final image = event!.debugMeta!.images.first; - expect('/path/to/app.exe', image.codeFile); + expect('/apex/com.android.art/javalib/arm64/boot.oat', image.codeFile); expect('13577ce71153c228ecf0eb73fc39f45010d487f8', image.codeId); expect('0x6f80b000', image.imageAddr); expect(3092480, image.imageSize); @@ -103,32 +102,12 @@ void main() { await fixture.hub.captureMessage('error'); verifyNever(fixture.binding.loadDebugImages()); }); - - test('ELF image is added on Windows for obfuscated stack trace', () async { - final ep = fixture.options.eventProcessors.first; - - final fs = MemoryFileSystem.test(); - (ep as dynamic).fs = fs; - await fs.directory('/path/to/data').create(recursive: true); - await fs.file('/path/to/data/app.so').writeAsString('12345'); - - final event = await ep.apply(_getEvent(), Hint()); - - final images = event!.debugMeta!.images; - expect(images.length, 2); - - final image = images.last; - expect(image.codeFile, '/path/to/data/app.so'); - expect(image.codeId, 'foo'); - expect(image.imageAddr, '0x123'); - expect(image.imageSize, 5); - }); }); } SentryEvent _getEvent() { final frame = SentryStackFrame(platform: 'native'); - final st = SentryStackTrace( - frames: [frame], nativeImageBaseAddr: '0x123', nativeBuildId: 'foo'); + final st = SentryStackTrace(frames: [frame]); return SentryEvent(threads: [SentryThread(stacktrace: st)]); } + \ No newline at end of file diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index feb97b5927..8f59d9cc4a 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -4,18 +4,17 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i7; -import 'dart:typed_data' as _i12; +import 'dart:typed_data' as _i11; import 'package:flutter/services.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i8; -import 'package:sentry/src/metrics/metric.dart' as _i14; +import 'package:sentry/src/metrics/metric.dart' as _i13; import 'package:sentry/src/metrics/metrics_api.dart' as _i5; import 'package:sentry/src/profiling.dart' as _i9; import 'package:sentry/src/sentry_tracer.dart' as _i3; import 'package:sentry_flutter/sentry_flutter.dart' as _i2; -import 'package:sentry_flutter/src/native/native_app_start.dart' as _i11; -import 'package:sentry_flutter/src/native/native_frames.dart' as _i13; +import 'package:sentry_flutter/src/native/native_frames.dart' as _i12; import 'package:sentry_flutter/src/native/sentry_native_binding.dart' as _i10; import 'mocks.dart' as _i6; @@ -280,6 +279,12 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { returnValueForMissingStub: null, ); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override _i2.SentrySpanContext get context => (super.noSuchMethod( Invocation.getter(#context), @@ -349,12 +354,6 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { returnValue: {}, ) as Map); - @override - Map get measurements => (super.noSuchMethod( - Invocation.getter(#measurements), - returnValue: {}, - ) as Map); - @override _i7.Future finish({ _i2.SpanStatus? status, @@ -519,6 +518,24 @@ class MockSentryTracer extends _i1.Mock implements _i3.SentryTracer { returnValueForMissingStub: null, ); + @override + void setMeasurementFromChild( + String? name, + num? value, { + _i2.SentryMeasurementUnit? unit, + }) => + super.noSuchMethod( + Invocation.method( + #setMeasurementFromChild, + [ + name, + value, + ], + {#unit: unit}, + ), + returnValueForMissingStub: null, + ); + @override void scheduleFinish() => super.noSuchMethod( Invocation.method( @@ -1103,191 +1120,110 @@ class MockSentryNativeBinding extends _i1.Mock } @override - _i7.Future init(_i2.Hub? hub) => (super.noSuchMethod( - Invocation.method( - #init, - [hub], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + bool get supportsCaptureEnvelope => (super.noSuchMethod( + Invocation.getter(#supportsCaptureEnvelope), + returnValue: false, + ) as bool); @override - _i7.Future close() => (super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + bool get supportsLoadContexts => (super.noSuchMethod( + Invocation.getter(#supportsLoadContexts), + returnValue: false, + ) as bool); @override - _i7.Future<_i11.NativeAppStart?> fetchNativeAppStart() => (super.noSuchMethod( - Invocation.method( - #fetchNativeAppStart, - [], - ), - returnValue: _i7.Future<_i11.NativeAppStart?>.value(), - ) as _i7.Future<_i11.NativeAppStart?>); + _i7.FutureOr init(_i2.Hub? hub) => + (super.noSuchMethod(Invocation.method( + #init, + [hub], + )) as _i7.FutureOr); @override - _i7.Future captureEnvelope( - _i12.Uint8List? envelopeData, + _i7.FutureOr captureEnvelope( + _i11.Uint8List? envelopeData, bool? containsUnhandledException, ) => - (super.noSuchMethod( - Invocation.method( - #captureEnvelope, - [ - envelopeData, - containsUnhandledException, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future beginNativeFrames() => (super.noSuchMethod( - Invocation.method( - #beginNativeFrames, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future<_i13.NativeFrames?> endNativeFrames(_i2.SentryId? id) => - (super.noSuchMethod( - Invocation.method( - #endNativeFrames, - [id], - ), - returnValue: _i7.Future<_i13.NativeFrames?>.value(), - ) as _i7.Future<_i13.NativeFrames?>); - - @override - _i7.Future setUser(_i2.SentryUser? user) => (super.noSuchMethod( - Invocation.method( - #setUser, - [user], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future addBreadcrumb(_i2.Breadcrumb? breadcrumb) => - (super.noSuchMethod( - Invocation.method( - #addBreadcrumb, - [breadcrumb], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #captureEnvelope, + [ + envelopeData, + containsUnhandledException, + ], + )) as _i7.FutureOr); @override - _i7.Future clearBreadcrumbs() => (super.noSuchMethod( - Invocation.method( - #clearBreadcrumbs, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr<_i12.NativeFrames?> endNativeFrames(_i2.SentryId? id) => + (super.noSuchMethod(Invocation.method( + #endNativeFrames, + [id], + )) as _i7.FutureOr<_i12.NativeFrames?>); @override - _i7.Future?> loadContexts() => (super.noSuchMethod( - Invocation.method( - #loadContexts, - [], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); + _i7.FutureOr addBreadcrumb(_i2.Breadcrumb? breadcrumb) => + (super.noSuchMethod(Invocation.method( + #addBreadcrumb, + [breadcrumb], + )) as _i7.FutureOr); @override - _i7.Future setContexts( + _i7.FutureOr setContexts( String? key, dynamic value, ) => - (super.noSuchMethod( - Invocation.method( - #setContexts, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setContexts, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeContexts(String? key) => (super.noSuchMethod( - Invocation.method( - #removeContexts, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeContexts(String? key) => + (super.noSuchMethod(Invocation.method( + #removeContexts, + [key], + )) as _i7.FutureOr); @override - _i7.Future setExtra( + _i7.FutureOr setExtra( String? key, dynamic value, ) => - (super.noSuchMethod( - Invocation.method( - #setExtra, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setExtra, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeExtra(String? key) => (super.noSuchMethod( - Invocation.method( - #removeExtra, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeExtra(String? key) => + (super.noSuchMethod(Invocation.method( + #removeExtra, + [key], + )) as _i7.FutureOr); @override - _i7.Future setTag( + _i7.FutureOr setTag( String? key, String? value, ) => - (super.noSuchMethod( - Invocation.method( - #setTag, - [ - key, - value, - ], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #setTag, + [ + key, + value, + ], + )) as _i7.FutureOr); @override - _i7.Future removeTag(String? key) => (super.noSuchMethod( - Invocation.method( - #removeTag, - [key], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr removeTag(String? key) => + (super.noSuchMethod(Invocation.method( + #removeTag, + [key], + )) as _i7.FutureOr); @override int? startProfiler(_i2.SentryId? traceId) => @@ -1297,74 +1233,38 @@ class MockSentryNativeBinding extends _i1.Mock )) as int?); @override - _i7.Future discardProfiler(_i2.SentryId? traceId) => - (super.noSuchMethod( - Invocation.method( - #discardProfiler, - [traceId], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); - - @override - _i7.Future displayRefreshRate() => (super.noSuchMethod( - Invocation.method( - #displayRefreshRate, - [], - ), - returnValue: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr discardProfiler(_i2.SentryId? traceId) => + (super.noSuchMethod(Invocation.method( + #discardProfiler, + [traceId], + )) as _i7.FutureOr); @override - _i7.Future?> collectProfile( + _i7.FutureOr?> collectProfile( _i2.SentryId? traceId, int? startTimeNs, int? endTimeNs, ) => - (super.noSuchMethod( - Invocation.method( - #collectProfile, - [ - traceId, - startTimeNs, - endTimeNs, - ], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); - - @override - _i7.Future?> loadDebugImages() => (super.noSuchMethod( - Invocation.method( - #loadDebugImages, - [], - ), - returnValue: _i7.Future?>.value(), - ) as _i7.Future?>); - - @override - _i7.Future pauseAppHangTracking() => (super.noSuchMethod( - Invocation.method( - #pauseAppHangTracking, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + (super.noSuchMethod(Invocation.method( + #collectProfile, + [ + traceId, + startTimeNs, + endTimeNs, + ], + )) as _i7.FutureOr?>); @override - _i7.Future resumeAppHangTracking() => (super.noSuchMethod( - Invocation.method( - #resumeAppHangTracking, - [], - ), - returnValue: _i7.Future.value(), - returnValueForMissingStub: _i7.Future.value(), - ) as _i7.Future); + _i7.FutureOr?> loadDebugImages( + _i2.SentryStackTrace? stackTrace) => + (super.noSuchMethod(Invocation.method( + #loadDebugImages, + [stackTrace], + )) as _i7.FutureOr?>); @override - _i7.Future<_i2.SentryId> captureReplay(bool? isCrash) => (super.noSuchMethod( + _i7.FutureOr<_i2.SentryId> captureReplay(bool? isCrash) => + (super.noSuchMethod( Invocation.method( #captureReplay, [isCrash], @@ -1376,7 +1276,7 @@ class MockSentryNativeBinding extends _i1.Mock [isCrash], ), )), - ) as _i7.Future<_i2.SentryId>); + ) as _i7.FutureOr<_i2.SentryId>); } /// A class which mocks [Hub]. @@ -1715,7 +1615,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i7.Future<_i2.SentryId> captureMetrics( - Map>? metricsBuckets) => + Map>? metricsBuckets) => (super.noSuchMethod( Invocation.method( #captureMetrics, diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index f2da4c737c..12ac9af707 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -3,8 +3,10 @@ library flutter_test; import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:ffi/ffi.dart'; +import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry/src/platform/platform.dart' as platform; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -29,7 +31,8 @@ void main() { setUp(() { options = SentryFlutterOptions(dsn: fakeDsn) // ignore: invalid_use_of_internal_member - ..automatedTestMode = true; + ..automatedTestMode = true + ..fileSystem = MemoryFileSystem.test(); sut = createBinding(options) as SentryNative; }); @@ -200,7 +203,7 @@ void main() { }); test('loadDebugImages', () async { - final list = await sut.loadDebugImages(); + final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); expect(list, isNotEmpty); expect(list![0].type, 'pe'); expect(list[0].debugId!.length, greaterThan(30)); @@ -215,6 +218,32 @@ void main() { (File file) => file.existsSync(), ); }); + + test('loadDebugImages adds app.so on Windows for obfuscated stack trace', + () async { + final list = await sut.loadDebugImages(SentryStackTrace( + frames: [], + // ignore: invalid_use_of_internal_member + nativeBuildId: '4c6950bd9e9cc9839071742a7295c09e', + // ignore: invalid_use_of_internal_member + nativeImageBaseAddr: '0x123', + )); + + await options.fileSystem.directory('/path/to/data').create(recursive: true); + await options.fileSystem + .file('/path/to/data/app.so') + .writeAsString('12345'); + + expect(list, isNotNull); + expect(list!.length, greaterThan(1)); + + final image = list.last; + expect(image.codeFile, '/path/to/data/app.so'); + expect(image.codeId, '4c6950bd9e9cc9839071742a7295c09e'); + expect(image.debugId, 'bd50694c-9c9e-83c9-9071-742a7295c09e'); + expect(image.imageAddr, '0x123'); + expect(image.imageSize, 5); + }); } /// Runs [command] with command's stdout and stderr being forwrarded to diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index fb339b4682..93f44b31ee 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -280,7 +280,7 @@ void main() { when(channel.invokeMethod('loadImageList')) .thenAnswer((invocation) async => json); - final data = await sut.loadDebugImages(); + final data = await sut.loadDebugImages(SentryStackTrace(frames: [])); expect(data?.map((v) => v.toJson()), json); }); From 7ab7ff07bc09aae321451331c9aa2521c538c4b1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 14:47:41 +0200 Subject: [PATCH 22/55] fix native test setup --- .../sentry_native/sentry_native_test.dart | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 12ac9af707..6047b0d0cb 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -16,10 +16,12 @@ import 'package:sentry_flutter/src/native/factory.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; +late final String repoRootDir; + void main() { - if (Directory.current.path.endsWith('/test')) { - Directory.current = Directory.current.parent; - } + repoRootDir = Directory.current.path.endsWith('/test') + ? Directory.current.parent.path + : Directory.current.path; setUpAll(() async { Directory.current = await _buildSentryNative('temp/native-test'); @@ -269,19 +271,22 @@ Future _exec(String executable, List arguments) async { Future _buildSentryNative(String nativeTestRoot) async { final cmakeBuildDir = '$nativeTestRoot/build'; final cmakeConfDir = '$nativeTestRoot/conf'; - final buildOutputDir = '$cmakeBuildDir/_deps/sentry-native-build/Debug/'; + final buildOutputDir = '$cmakeBuildDir/_deps/sentry-native-build/Release/'; if (!_builtVersionIsExpected(buildOutputDir)) { Directory(cmakeConfDir).createSync(recursive: true); + File('$cmakeConfDir/main.c').writeAsStringSync(''' +int main(int argc, char *argv[]) { return 0; } +'''); File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' cmake_minimum_required(VERSION 3.14) project(sentry-native-flutter-test) add_subdirectory(../../../${platform.instance.operatingSystem} plugin) -add_library(\${CMAKE_PROJECT_NAME} INTERFACE) -target_link_libraries(\${CMAKE_PROJECT_NAME} INTERFACE \${sentry_flutter_bundled_libraries}) +add_executable(\${CMAKE_PROJECT_NAME} main.c) +target_link_libraries(\${CMAKE_PROJECT_NAME} PRIVATE sentry_flutter_plugin) '''); await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); - await _exec('cmake', ['--build', cmakeBuildDir]); + await _exec('cmake', ['--build', cmakeBuildDir, '--config', 'Release']); } return buildOutputDir; } @@ -299,7 +304,8 @@ bool _builtVersionIsExpected(String buildOutputDir) { return File('$buildOutputDir/sentry.dll').existsSync(); } -late final _configuredSentryNativeVersion = File('sentry-native/CMakeCache.txt') +late final _configuredSentryNativeVersion = + File('$repoRootDir/sentry-native/CMakeCache.txt') .readAsLinesSync() .map((line) => line.startsWith('version=') ? line.substring(8) : null) .firstWhere((line) => line != null)!; From b51696179e734b265d2406efa7ba096cf52b3699 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 15:02:52 +0200 Subject: [PATCH 23/55] further improve native test build --- .../sentry_native/sentry_native_test.dart | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 6047b0d0cb..f230e1bf13 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -24,7 +24,8 @@ void main() { : Directory.current.path; setUpAll(() async { - Directory.current = await _buildSentryNative('temp/native-test'); + Directory.current = + await _buildSentryNative('$repoRootDir/temp/native-test'); }); late SentryNative sut; @@ -34,6 +35,7 @@ void main() { options = SentryFlutterOptions(dsn: fakeDsn) // ignore: invalid_use_of_internal_member ..automatedTestMode = true + ..debug = true ..fileSystem = MemoryFileSystem.test(); sut = createBinding(options) as SentryNative; }); @@ -187,18 +189,10 @@ void main() { throwsUnsupportedError); }); - // test('captureEnvelope', () async { - // final data = Uint8List.fromList([1, 2, 3]); - - // late Uint8List captured; - // when(channel.invokeMethod('captureEnvelope', any)).thenAnswer( - // (invocation) async => - // {captured = invocation.positionalArguments[1][0] as Uint8List}); - - // await sut.captureEnvelope(data, false); - - // expect(captured, data); - // }); + test('captureEnvelope', () async { + final data = Uint8List.fromList([1, 2, 3]); + expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); + }); test('loadContexts', () async { expect(await sut.loadContexts(), isNull); @@ -271,10 +265,11 @@ Future _exec(String executable, List arguments) async { Future _buildSentryNative(String nativeTestRoot) async { final cmakeBuildDir = '$nativeTestRoot/build'; final cmakeConfDir = '$nativeTestRoot/conf'; - final buildOutputDir = '$cmakeBuildDir/_deps/sentry-native-build/Release/'; + final buildOutputDir = '$nativeTestRoot/dist/'; - if (!_builtVersionIsExpected(buildOutputDir)) { + if (!_builtVersionIsExpected(cmakeBuildDir, buildOutputDir)) { Directory(cmakeConfDir).createSync(recursive: true); + Directory(buildOutputDir).createSync(recursive: true); File('$cmakeConfDir/main.c').writeAsStringSync(''' int main(int argc, char *argv[]) { return 0; } '''); @@ -284,15 +279,22 @@ project(sentry-native-flutter-test) add_subdirectory(../../../${platform.instance.operatingSystem} plugin) add_executable(\${CMAKE_PROJECT_NAME} main.c) target_link_libraries(\${CMAKE_PROJECT_NAME} PRIVATE sentry_flutter_plugin) + +# Same as generated_plugins.cmake +list(APPEND PLUGIN_BUNDLED_LIBRARIES \$) +list(APPEND PLUGIN_BUNDLED_LIBRARIES \${sentry_flutter_bundled_libraries}) +install(FILES "\${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${buildOutputDir.replaceAll('\\', '/')}" COMPONENT Runtime) '''); await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); await _exec('cmake', ['--build', cmakeBuildDir, '--config', 'Release']); + await _exec('cmake', ['--install', cmakeBuildDir, '--config', 'Release']); } return buildOutputDir; } -bool _builtVersionIsExpected(String buildOutputDir) { - final buildCmake = File('$buildOutputDir/../sentry-config-version.cmake'); +bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { + final buildCmake = File( + '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); if (!buildCmake.existsSync()) return false; if (!buildCmake @@ -301,11 +303,12 @@ bool _builtVersionIsExpected(String buildOutputDir) { return false; } - return File('$buildOutputDir/sentry.dll').existsSync(); + return File('$buildOutputDir/sentry.dll').existsSync() && + File('$buildOutputDir/crashpad_handler.exe').existsSync(); } late final _configuredSentryNativeVersion = File('$repoRootDir/sentry-native/CMakeCache.txt') - .readAsLinesSync() - .map((line) => line.startsWith('version=') ? line.substring(8) : null) - .firstWhere((line) => line != null)!; + .readAsLinesSync() + .map((line) => line.startsWith('version=') ? line.substring(8) : null) + .firstWhere((line) => line != null)!; From 9a7f1a96f928711ddd66a661774a5d417323529c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 15:47:34 +0200 Subject: [PATCH 24/55] update debug id conversion code --- flutter/lib/src/native/c/sentry_native.dart | 67 +++++++++---------- .../sentry_native/sentry_native_test.dart | 29 ++++---- 2 files changed, 47 insertions(+), 49 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 1b028c378e..8d731a0cbe 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -214,7 +214,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { // On windows, we need to add the ELF debug image of the AOT code. // See https://github.com/flutter/flutter/issues/154840 if (options.platformChecker.platform.isWindows) { - _appDebugImage ??= await _getAppDebugImage(stackTrace, images); + _appDebugImage ??= await getAppDebugImage(stackTrace, images); if (_appDebugImage != null) { images.add(_appDebugImage!); } @@ -226,7 +226,8 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { } }); - Future _getAppDebugImage( + @visibleForTesting + Future getAppDebugImage( SentryStackTrace stackTrace, Iterable nativeImages) async { // ignore: invalid_use_of_internal_member final buildId = stackTrace.nativeBuildId; @@ -260,21 +261,6 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return null; } - // Symbolicator requires Debug ID to find the debug image. We can construct - // it from the buildID: https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 - // For example - // Build ID: 4c6950bd9e9cc9839071742a7295c09e - // Debug ID: bd50694c-9c9e-83c9-9071-742a7295c09e - String? debugId; - if (buildId.length == 16) { - final p = _DwarfDebugIdBuilder(buildId); - debugId = - "${p.take(4)}-${p.take(2)}-${p.take(2)}-${p.take(2, false)}-${p.take(6, false)}"; - } else { - options.logger(SentryLevel.debug, - "Couldn't construct AOT ELF image DebugID because ${appSoFile.path} doesn't exist."); - } - final stat = await appSoFile.stat(); return DebugImage( type: 'elf', @@ -282,10 +268,39 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { imageSize: stat.size, codeFile: appSoFile.path, codeId: buildId, - debugId: debugId, + debugId: _computeDebugId(buildId), ); } + /// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 + /// Converts an ELF object identifier into a `DebugId`. + /// + /// The identifier data is first truncated or extended to match 16 byte size of + /// Uuids. If the data is declared in little endian, the first three Uuid fields + /// are flipped to match the big endian expected by the breakpad processor. + /// + /// The `DebugId::appendix` field is always `0` for ELF. + String? _computeDebugId(String buildId) { + // Make sure that we have exactly UUID_SIZE bytes available + const uuidSize = 16 * 2; + final data = Uint8List(uuidSize); + final len = buildId.length.clamp(0, uuidSize); + data.setAll(0, buildId.codeUnits.take(len)); + + if (Endian.host == Endian.little) { + // The file ELF file targets a little endian architecture. Convert to + // network byte order (big endian) to match the Breakpad processor's + // expectations. For big endian object files, this is not needed. + // To manipulate this as hex, we create an Uint16 view. + final data16 = Uint16List.view(data.buffer); + data16.setRange(0, 4, data16.sublist(0, 4).reversed); + data16.setRange(4, 6, data16.sublist(4, 6).reversed); + data16.setRange(6, 8, data16.sublist(6, 8).reversed); + } + + return String.fromCharCodes(data); + } + FutureOr pauseAppHangTracking() {} FutureOr resumeAppHangTracking() {} @@ -422,19 +437,3 @@ extension on List { return cObject; } } - -// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 -class _DwarfDebugIdBuilder { - Iterable> _chunks; - - _DwarfDebugIdBuilder(String buildId) : _chunks = buildId.split('').slices(2); - - String take(int numChunks, [bool reversed = true]) { - var part = _chunks.take(numChunks); - _chunks = _chunks.skip(numChunks); - if (reversed) { - part = part.toList().reversed; - } - return part.map((chunk) => chunk.join()).join(); - } -} diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index f230e1bf13..34d5d55a1e 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -215,28 +215,27 @@ void main() { ); }); - test('loadDebugImages adds app.so on Windows for obfuscated stack trace', - () async { - final list = await sut.loadDebugImages(SentryStackTrace( - frames: [], - // ignore: invalid_use_of_internal_member - nativeBuildId: '4c6950bd9e9cc9839071742a7295c09e', - // ignore: invalid_use_of_internal_member - nativeImageBaseAddr: '0x123', - )); - + test('getAppDebugImage returns app.so debug image', () async { await options.fileSystem.directory('/path/to/data').create(recursive: true); await options.fileSystem .file('/path/to/data/app.so') .writeAsString('12345'); - expect(list, isNotNull); - expect(list!.length, greaterThan(1)); + final image = await sut.getAppDebugImage( + SentryStackTrace( + frames: [], + // ignore: invalid_use_of_internal_member + nativeBuildId: '4c6950bd9e9cc9839071742a7295c09e', + // ignore: invalid_use_of_internal_member + nativeImageBaseAddr: '0x123', + ), + [DebugImage(type: 'pe', codeFile: '/path/to/application.exe')], + ); - final image = list.last; - expect(image.codeFile, '/path/to/data/app.so'); + expect(image, isNotNull); + expect(image!.codeFile, '/path/to/data/app.so'); expect(image.codeId, '4c6950bd9e9cc9839071742a7295c09e'); - expect(image.debugId, 'bd50694c-9c9e-83c9-9071-742a7295c09e'); + expect(image.debugId, 'bd50694c9c9e83c99071742a7295c09e'); expect(image.imageAddr, '0x123'); expect(image.imageSize, 5); }); From 9fb0c2a27a9f146c0dda71bb8dbe401699694823 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 20:19:03 +0200 Subject: [PATCH 25/55] fix native init test --- flutter/ffi-native.yaml | 1 + flutter/lib/src/native/c/binding.dart | 31 +++++++++++++++++++ flutter/lib/src/native/c/sentry_native.dart | 14 +++++++-- .../sentry_native/sentry_native_test.dart | 6 ++-- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index 0e5b3c0e5a..4d59bfaa26 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -18,6 +18,7 @@ functions: - sentry_options_set_auto_session_tracking - sentry_options_set_dist - sentry_options_set_max_breadcrumbs + - sentry_options_set_handler_path - sentry_set_user - sentry_remove_user - sentry_add_breadcrumb diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index 3d2af5026d..059d9c9eac 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -606,6 +606,37 @@ class SentryNative { _options_get_auto_session_trackingPtr .asFunction)>(); + /// Sets the path to the crashpad handler if the crashpad backend is used. + /// + /// The path defaults to the `crashpad_handler`/`crashpad_handler.exe` + /// executable, depending on platform, which is expected to be present in the + /// same directory as the app executable. + /// + /// It is recommended that library users set an explicit handler path, depending + /// on the directory/executable structure of their app. + /// + /// `path` is assumed to be in platform-specific filesystem path encoding. + /// API Users on windows are encouraged to use `sentry_options_set_handler_pathw` + /// instead. + void options_set_handler_path( + ffi.Pointer opts, + ffi.Pointer path, + ) { + return _options_set_handler_path( + opts, + path, + ); + } + + late final _options_set_handler_pathPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer, + ffi.Pointer)>>('sentry_options_set_handler_path'); + late final _options_set_handler_path = + _options_set_handler_pathPtr.asFunction< + void Function( + ffi.Pointer, ffi.Pointer)>(); + /// Initializes the Sentry SDK with the specified options. /// /// This takes ownership of the options. After the options have been set diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 8d731a0cbe..9b60a15d62 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -23,6 +23,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { static late final native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); + @visibleForTesting + static String? crashpadPath; + SentryNative(this.options); void _logNotSupported(String operation) => options.logger( @@ -63,13 +66,20 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { options.logger(SentryLevel.warning, 'SentryNative: setting a proxy is currently not supported'); } + + if (crashpadPath != null) { + native.options_set_handler_path(cOptions, c.str(crashpadPath)); + } + return cOptions; } finally { c.freeAll(); } } - FutureOr close() => native.close(); + FutureOr close() { + tryCatchSync('close', native.close); + } FutureOr fetchNativeAppStart() => null; @@ -166,7 +176,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { } FutureOr removeTag(String key) { - tryCatchSync('set_tag', () { + tryCatchSync('remove_tag', () { final cKey = key.toNativeUtf8(); native.remove_tag(cKey.cast()); malloc.free(cKey); diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 34d5d55a1e..5c104fbacc 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -26,6 +26,8 @@ void main() { setUpAll(() async { Directory.current = await _buildSentryNative('$repoRootDir/temp/native-test'); + SentryNative.crashpadPath = + '${Directory.current.path}/crashpad_handler.exe'; }); late SentryNative sut; @@ -40,8 +42,6 @@ void main() { sut = createBinding(options) as SentryNative; }); - tearDown(() => sut.close()); - test('options', () { options ..debug = true @@ -91,7 +91,7 @@ void main() { }); test('init', () async { - // There's nothing we can check here - just that it doesn't crash. + addTearDown(sut.close); await sut.init(MockHub()); }); From 2f093db33c0fdfaa704852b03f543217b8c57553 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 12 Sep 2024 21:01:11 +0200 Subject: [PATCH 26/55] test fixes --- .../src/integrations/native_sdk_integration.dart | 8 ++++++++ .../test/integrations/load_image_list_test.dart | 11 +++++------ .../integrations/native_sdk_integration_test.dart | 8 ++++++++ flutter/test/native_scope_observer_test.dart | 10 ++++++++++ flutter/test/profiling_test.dart | 2 ++ flutter/test/sentry_flutter_test.dart | 14 +++++++++----- 6 files changed, 42 insertions(+), 11 deletions(-) diff --git a/flutter/lib/src/integrations/native_sdk_integration.dart b/flutter/lib/src/integrations/native_sdk_integration.dart index ad77711b63..d1a4453a13 100644 --- a/flutter/lib/src/integrations/native_sdk_integration.dart +++ b/flutter/lib/src/integrations/native_sdk_integration.dart @@ -29,6 +29,10 @@ class NativeSdkIntegration implements Integration { exception: exception, stackTrace: stackTrace, ); + // ignore: invalid_use_of_internal_member + if (_options?.automatedTestMode ?? false) { + rethrow; + } } } @@ -44,6 +48,10 @@ class NativeSdkIntegration implements Integration { exception: exception, stackTrace: stackTrace, ); + // ignore: invalid_use_of_internal_member + if (_options?.automatedTestMode ?? false) { + rethrow; + } } } } diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 5015990985..1788d187b1 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -26,7 +26,7 @@ void main() { setUp(() async { fixture = IntegrationTestFixture(LoadImageListIntegration.new); - when(fixture.binding.loadDebugImages()) + when(fixture.binding.loadDebugImages(any)) .thenAnswer((_) async => imageList); await fixture.registerIntegration(); }); @@ -44,14 +44,14 @@ void main() { await fixture.hub.captureException(StateError('error'), stackTrace: StackTrace.current); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); test('Native layer is not called if the event has no stack traces', () async { await fixture.hub.captureException(StateError('error')); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); test('Native layer is called because stack traces are not symbolicated', @@ -67,7 +67,7 @@ void main() { #01 abs 000000723d637527 virt 00000000001f0527 _kDartIsolateSnapshotInstructions+0x1e5527 '''); - verify(fixture.binding.loadDebugImages()).called(1); + verify(fixture.binding.loadDebugImages(any)).called(1); }); test('Event processor adds image list to the event', () async { @@ -100,7 +100,7 @@ void main() { expect(fixture.options.eventProcessors.length, 1); await fixture.hub.captureMessage('error'); - verifyNever(fixture.binding.loadDebugImages()); + verifyNever(fixture.binding.loadDebugImages(any)); }); }); } @@ -110,4 +110,3 @@ SentryEvent _getEvent() { final st = SentryStackTrace(frames: [frame]); return SentryEvent(threads: [SentryThread(stacktrace: st)]); } - \ No newline at end of file diff --git a/flutter/test/integrations/native_sdk_integration_test.dart b/flutter/test/integrations/native_sdk_integration_test.dart index 5c244b3d66..9a211cd70f 100644 --- a/flutter/test/integrations/native_sdk_integration_test.dart +++ b/flutter/test/integrations/native_sdk_integration_test.dart @@ -15,6 +15,8 @@ void main() { setUp(() { fixture = IntegrationTestFixture(NativeSdkIntegration.new); + when(fixture.binding.init(any)).thenReturn(() {}); + when(fixture.binding.close()).thenReturn(() {}); }); test('adds integration', () async { @@ -25,6 +27,9 @@ void main() { }); test('do not throw', () async { + // ignore: invalid_use_of_internal_member + fixture.options.automatedTestMode = false; + fixture.sut = NativeSdkIntegration(_ThrowingMockSentryNative()); await fixture.registerIntegration(); expect(fixture.options.sdk.integrations.contains('nativeSdkIntegration'), @@ -51,6 +56,9 @@ void main() { }); test(' is not added in case of an exception', () async { + // ignore: invalid_use_of_internal_member + fixture.options.automatedTestMode = false; + fixture.sut = NativeSdkIntegration(_ThrowingMockSentryNative()); await fixture.registerIntegration(); expect(fixture.options.sdk.integrations, []); diff --git a/flutter/test/native_scope_observer_test.dart b/flutter/test/native_scope_observer_test.dart index 9d7a8fd2a9..ea0cfbf425 100644 --- a/flutter/test/native_scope_observer_test.dart +++ b/flutter/test/native_scope_observer_test.dart @@ -18,6 +18,7 @@ void main() { }); test('addBreadcrumbCalls', () async { + when(mock.addBreadcrumb(any)).thenReturn(() {}); final breadcrumb = Breadcrumb(); await sut.addBreadcrumb(breadcrumb); @@ -25,12 +26,14 @@ void main() { }); test('clearBreadcrumbsCalls', () async { + when(mock.clearBreadcrumbs()).thenReturn(() {}); await sut.clearBreadcrumbs(); verify(mock.clearBreadcrumbs()).called(1); }); test('removeContextsCalls', () async { + when(mock.removeContexts(any)).thenReturn(() {}); await sut.removeContexts('fixture-key'); expect( @@ -38,36 +41,43 @@ void main() { }); test('removeExtraCalls', () async { + when(mock.removeExtra(any)).thenReturn(() {}); await sut.removeExtra('fixture-key'); expect(verify(mock.removeExtra(captureAny)).captured.single, 'fixture-key'); }); test('removeTagCalls', () async { + when(mock.removeTag(any)).thenReturn(() {}); await sut.removeTag('fixture-key'); expect(verify(mock.removeTag(captureAny)).captured.single, 'fixture-key'); }); test('setContextsCalls', () async { + when(mock.setContexts(any, any)).thenReturn(() {}); await sut.setContexts('fixture-key', 'fixture-value'); verify(mock.setContexts('fixture-key', 'fixture-value')).called(1); }); test('setExtraCalls', () async { + when(mock.setExtra(any, any)).thenReturn(() {}); await sut.setExtra('fixture-key', 'fixture-value'); verify(mock.setExtra('fixture-key', 'fixture-value')).called(1); }); test('setTagCalls', () async { + when(mock.setTag(any, any)).thenReturn(() {}); await sut.setTag('fixture-key', 'fixture-value'); verify(mock.setTag('fixture-key', 'fixture-value')).called(1); }); test('setUserCalls', () async { + when(mock.setUser(any)).thenReturn(() {}); + final user = SentryUser(id: 'foo bar'); await sut.setUser(user); diff --git a/flutter/test/profiling_test.dart b/flutter/test/profiling_test.dart index 04ff70d065..15beafd774 100644 --- a/flutter/test/profiling_test.dart +++ b/flutter/test/profiling_test.dart @@ -68,6 +68,8 @@ void main() { }); test('dispose() calls native discard() exactly once', () async { + when(mock.discardProfiler(any)).thenReturn(() {}); + sut.dispose(); sut.dispose(); // Additional calls must not have an effect. diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 05106d16d4..6dbb40f4d2 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -242,8 +242,7 @@ void main() { ); testScopeObserver( - options: sentryFlutterOptions!, - expectedHasNativeScopeObserver: true); + options: sentryFlutterOptions!, expectedHasNativeScopeObserver: true); testConfiguration( integrations: integrations, @@ -630,7 +629,6 @@ void main() { test( 'enablePureDartSymbolication is set to false during SentryFlutter init', () async { - SentryFlutter.native = MockSentryNativeBinding(); await SentryFlutter.init( (options) { options.dsn = fakeDsn; @@ -650,7 +648,7 @@ void main() { }); test('resumeAppHangTracking calls native method when available', () async { - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); when(SentryFlutter.native?.resumeAppHangTracking()) .thenAnswer((_) => Future.value()); @@ -669,7 +667,7 @@ void main() { }); test('pauseAppHangTracking calls native method when available', () async { - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); when(SentryFlutter.native?.pauseAppHangTracking()) .thenAnswer((_) => Future.value()); @@ -734,6 +732,12 @@ void main() { }); } +MockSentryNativeBinding mockNativeBinding() { + final result = MockSentryNativeBinding(); + when(result.supportsLoadContexts).thenReturn(true); + return result; +} + void appRunner() {} void loadTestPackage() { From 5456fac170af94053e6154dce43d94334c4d98e5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 16 Sep 2024 17:29:27 +0200 Subject: [PATCH 27/55] cleanup --- flutter/lib/src/integrations/load_image_list_integration.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 640b7be4b3..891f2e6333 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -5,10 +5,6 @@ import 'package:sentry/sentry.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; -// ignore: implementation_imports -import 'package:sentry/src/load_dart_debug_images_integration.dart' - show NeedsSymbolication; - /// Loads the native debug image list for stack trace symbolication. class LoadImageListIntegration extends Integration { /// TODO: rename to LoadNativeDebugImagesIntegration in the next major version From 4c6ddd054265bf101a11f665c0a39bf2138d91bc Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 16 Sep 2024 20:47:00 +0200 Subject: [PATCH 28/55] improve native test --- flutter/temp/native-test/dist/sentry.dll | Bin 280064 -> 0 bytes .../test/sentry_native/sentry_native_test.dart | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 flutter/temp/native-test/dist/sentry.dll diff --git a/flutter/temp/native-test/dist/sentry.dll b/flutter/temp/native-test/dist/sentry.dll deleted file mode 100644 index 21615e9b7d335ff796f8ef03339c05780c9e36f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 280064 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�-0)E$fH{&iSTzR59+v^ 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%0A8d$Wr2cmF@b#M0vCDY7zCl!lXFw{tv-~4gCcV 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 Date: Mon, 16 Sep 2024 21:00:56 +0200 Subject: [PATCH 29/55] ensure crashpad_wer is distributed --- flutter/sentry-native/sentry-native.cmake | 1 + .../sentry_native/sentry_native_test.dart | 21 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/flutter/sentry-native/sentry-native.cmake b/flutter/sentry-native/sentry-native.cmake index 346f4b1457..0a5fc7884f 100644 --- a/flutter/sentry-native/sentry-native.cmake +++ b/flutter/sentry-native/sentry-native.cmake @@ -20,6 +20,7 @@ FetchContent_MakeAvailable(sentry-native) # external build triggered from this build file. set(sentry_flutter_bundled_libraries $ + $ PARENT_SCOPE ) diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index dc4d207499..2e6d173277 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -17,17 +17,24 @@ import '../mocks.dart'; import '../mocks.mocks.dart'; late final String repoRootDir; +late final List expectedDistFiles; void main() { repoRootDir = Directory.current.path.endsWith('/test') ? Directory.current.parent.path : Directory.current.path; + expectedDistFiles = [ + 'sentry.dll', + 'crashpad_handler.exe', + 'crashpad_wer.dll', + ]; + setUpAll(() async { Directory.current = await _buildSentryNative('$repoRootDir/temp/native-test'); SentryNative.crashpadPath = - '${Directory.current.path}/crashpad_handler.exe'; + '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.startsWith('crashpad_handler'))}'; }); late SentryNative sut; @@ -42,6 +49,14 @@ void main() { sut = createBinding(options) as SentryNative; }); + test('expected output files', () { + for (var name in expectedDistFiles) { + if (!File(name).existsSync()) { + fail('Native distribution file $name does not exist'); + } + } + }); + test('options', () { options ..debug = true @@ -303,8 +318,8 @@ bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { return false; } - return File('$buildOutputDir/sentry.dll').existsSync() && - File('$buildOutputDir/crashpad_handler.exe').existsSync(); + return !expectedDistFiles + .any((name) => !File('$buildOutputDir/$name').existsSync()); } late final _configuredSentryNativeVersion = From b30ed39730d42a8fa24d87e8c08e750076914d66 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Mon, 16 Sep 2024 22:01:37 +0200 Subject: [PATCH 30/55] post-merge failing test fixes --- flutter/test/file_system_transport_test.dart | 4 ++++ flutter/test/initialization_test.dart | 6 ++---- flutter/test/sentry_flutter_test.dart | 11 +++++++---- flutter/test/sentry_navigator_observer_test.dart | 1 + 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/flutter/test/file_system_transport_test.dart b/flutter/test/file_system_transport_test.dart index 84bef4babe..91340f7fb6 100644 --- a/flutter/test/file_system_transport_test.dart +++ b/flutter/test/file_system_transport_test.dart @@ -159,6 +159,10 @@ class Fixture { final options = defaultTestOptions(); final binding = MockSentryNativeBinding(); + Fixture() { + when(binding.captureEnvelope(any, any)).thenReturn(null); + } + FileSystemTransport getSut() { return FileSystemTransport(binding, options); } diff --git a/flutter/test/initialization_test.dart b/flutter/test/initialization_test.dart index d36e549339..527b51b247 100644 --- a/flutter/test/initialization_test.dart +++ b/flutter/test/initialization_test.dart @@ -10,8 +10,6 @@ import 'mocks.dart'; // https://github.com/getsentry/sentry-dart/issues/508 // There are no asserts, test are succesfull if no exceptions are thrown. void main() { - final native = NativeChannelFixture(); - void optionsInitializer(SentryFlutterOptions options) { // LoadReleaseIntegration throws because package_info channel is not available options.removeIntegration( @@ -20,12 +18,12 @@ void main() { test('async re-initilization', () async { await SentryFlutter.init(optionsInitializer, - options: defaultTestOptions()..methodChannel = native.channel); + options: defaultTestOptions()..autoInitializeNativeSdk = false); await Sentry.close(); await SentryFlutter.init(optionsInitializer, - options: defaultTestOptions()..methodChannel = native.channel); + options: defaultTestOptions()..autoInitializeNativeSdk = false); await Sentry.close(); }); diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 80c119ee7a..d48a428b6b 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -229,7 +229,8 @@ void main() { Transport transport = MockTransport(); final sentryFlutterOptions = defaultTestOptions( getPlatformChecker(platform: MockPlatform.windows())) - ..methodChannel = native.channel; + // We need to disable native init because sentry.dll is not available here. + ..autoInitializeNativeSdk = false; await SentryFlutter.init( (options) async { @@ -271,8 +272,6 @@ void main() { expect(SentryFlutter.native, isNotNull); expect(Sentry.currentHub.profilerFactory, isNull); - - await Sentry.close(); }, testOn: 'vm'); test('Linux', () async { @@ -629,7 +628,7 @@ void main() { () async { final sentryFlutterOptions = defaultTestOptions( getPlatformChecker(platform: MockPlatform.android(), isWeb: true)); - SentryFlutter.native = MockSentryNativeBinding(); + SentryFlutter.native = mockNativeBinding(); await SentryFlutter.init( (options) { expect(options.enableDartSymbolication, false); @@ -724,6 +723,10 @@ void main() { MockSentryNativeBinding mockNativeBinding() { final result = MockSentryNativeBinding(); when(result.supportsLoadContexts).thenReturn(true); + when(result.supportsCaptureEnvelope).thenReturn(true); + when(result.captureEnvelope(any, any)).thenReturn(null); + when(result.init(any)).thenReturn(null); + when(result.close()).thenReturn(null); return result; } diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 524f3e2ece..66468aadf9 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -48,6 +48,7 @@ void main() { setUp(() { mockBinding = MockSentryNativeBinding(); + when(mockBinding.beginNativeFrames()).thenReturn(null); SentryFlutter.native = mockBinding; }); From 0098c637caf214172153fb6a693139fddb2bc58b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Tue, 17 Sep 2024 20:52:32 +0200 Subject: [PATCH 31/55] move stacktrace parsing back to stacktrace factory --- dart/lib/src/debug_image_extractor.dart | 195 ------------------ .../load_dart_debug_images_integration.dart | 146 ++++++++----- dart/lib/src/platform/platform.dart | 5 + dart/lib/src/protocol/sentry_event.dart | 7 + dart/lib/src/protocol/sentry_stack_frame.dart | 1 + dart/lib/src/protocol/sentry_stack_trace.dart | 12 +- dart/lib/src/sentry_client.dart | 2 - dart/lib/src/sentry_options.dart | 4 +- dart/lib/src/sentry_stack_trace_factory.dart | 30 +-- dart/test/debug_image_extractor_test.dart | 118 ----------- ...ad_dart_debug_images_integration_test.dart | 153 +++++++++++--- dart/test/mocks/mock_platform.dart | 14 +- dart/test/sentry_exception_factory_test.dart | 21 +- .../load_image_list_integration.dart | 12 +- flutter/lib/src/native/c/sentry_native.dart | 4 +- .../sentry_native/sentry_native_test.dart | 4 +- 16 files changed, 282 insertions(+), 446 deletions(-) delete mode 100644 dart/lib/src/debug_image_extractor.dart delete mode 100644 dart/test/debug_image_extractor_test.dart diff --git a/dart/lib/src/debug_image_extractor.dart b/dart/lib/src/debug_image_extractor.dart deleted file mode 100644 index 99776ee12b..0000000000 --- a/dart/lib/src/debug_image_extractor.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'dart:typed_data'; -import 'package:meta/meta.dart'; -import 'package:uuid/uuid.dart'; - -import '../sentry.dart'; - -// Regular expressions for parsing header lines -const String _headerStartLine = - '*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***'; -final RegExp _buildIdRegex = RegExp(r"build_id(?:=|: )'([\da-f]+)'"); -final RegExp _isolateDsoBaseLineRegex = - RegExp(r'isolate_dso_base(?:=|: )([\da-f]+)'); - -/// Extracts debug information from stack trace header. -/// Needed for symbolication of Dart stack traces without native debug images. -@internal -class DebugImageExtractor { - DebugImageExtractor(this._options); - - final SentryOptions _options; - - // We don't need to always parse the debug image, so we cache it here. - DebugImage? _debugImage; - - @visibleForTesting - DebugImage? get debugImageForTesting => _debugImage; - - DebugImage? extractFrom(String stackTraceString) { - if (_debugImage != null) { - return _debugImage; - } - _debugImage = _extractDebugInfoFrom(stackTraceString).toDebugImage(); - return _debugImage; - } - - _DebugInfo _extractDebugInfoFrom(String stackTraceString) { - String? buildId; - String? isolateDsoBase; - - final lines = stackTraceString.split('\n'); - - for (final line in lines) { - if (_isHeaderStartLine(line)) { - continue; - } - // Stop parsing as soon as we get to the stack frames - // This should never happen but is a safeguard to avoid looping - // through every line of the stack trace - if (line.contains("#00 abs")) { - break; - } - - buildId ??= _extractBuildId(line); - isolateDsoBase ??= _extractIsolateDsoBase(line); - - // Early return if all needed information is found - if (buildId != null && isolateDsoBase != null) { - return _DebugInfo(buildId, isolateDsoBase, _options); - } - } - - return _DebugInfo(buildId, isolateDsoBase, _options); - } - - bool _isHeaderStartLine(String line) { - return line.contains(_headerStartLine); - } - - String? _extractBuildId(String line) { - final buildIdMatch = _buildIdRegex.firstMatch(line); - return buildIdMatch?.group(1); - } - - String? _extractIsolateDsoBase(String line) { - final isolateMatch = _isolateDsoBaseLineRegex.firstMatch(line); - return isolateMatch?.group(1); - } -} - -class _DebugInfo { - final String? buildId; - final String? isolateDsoBase; - final SentryOptions _options; - - _DebugInfo(this.buildId, this.isolateDsoBase, this._options); - - DebugImage? toDebugImage() { - if (buildId == null || isolateDsoBase == null) { - _options.logger(SentryLevel.warning, - 'Cannot create DebugImage without buildId and isolateDsoBase.'); - return null; - } - - String type; - String? imageAddr; - String? debugId; - String? codeId; - - final platform = _options.platformChecker.platform; - - // Default values for all platforms - imageAddr = '0x$isolateDsoBase'; - - if (platform.isAndroid) { - type = 'elf'; - debugId = _convertCodeIdToDebugId(buildId!); - codeId = buildId; - } else if (platform.isIOS || platform.isMacOS) { - type = 'macho'; - debugId = _formatHexToUuid(buildId!); - // `codeId` is not needed for iOS/MacOS. - } else { - _options.logger( - SentryLevel.warning, - 'Unsupported platform for creating Dart debug images.', - ); - return null; - } - - return DebugImage( - type: type, - imageAddr: imageAddr, - debugId: debugId, - codeId: codeId, - ); - } - - // Debug identifier is the little-endian UUID representation of the first 16-bytes of - // the build ID on ELF images. - String? _convertCodeIdToDebugId(String codeId) { - codeId = codeId.replaceAll(' ', ''); - if (codeId.length < 32) { - _options.logger(SentryLevel.warning, - 'Code ID must be at least 32 hexadecimal characters long'); - return null; - } - - final first16Bytes = codeId.substring(0, 32); - final byteData = _parseHexToBytes(first16Bytes); - - if (byteData == null || byteData.isEmpty) { - _options.logger( - SentryLevel.warning, 'Failed to convert code ID to debug ID'); - return null; - } - - return bigToLittleEndianUuid(UuidValue.fromByteList(byteData).uuid); - } - - Uint8List? _parseHexToBytes(String hex) { - if (hex.length % 2 != 0) { - _options.logger( - SentryLevel.warning, 'Invalid hex string during debug image parsing'); - return null; - } - if (hex.startsWith('0x')) { - hex = hex.substring(2); - } - - var bytes = Uint8List(hex.length ~/ 2); - for (var i = 0; i < hex.length; i += 2) { - bytes[i ~/ 2] = int.parse(hex.substring(i, i + 2), radix: 16); - } - return bytes; - } - - String bigToLittleEndianUuid(String bigEndianUuid) { - final byteArray = - Uuid.parse(bigEndianUuid, validationMode: ValidationMode.nonStrict); - - final reversedByteArray = Uint8List.fromList([ - ...byteArray.sublist(0, 4).reversed, - ...byteArray.sublist(4, 6).reversed, - ...byteArray.sublist(6, 8).reversed, - ...byteArray.sublist(8, 10), - ...byteArray.sublist(10), - ]); - - return Uuid.unparse(reversedByteArray); - } - - String? _formatHexToUuid(String hex) { - if (hex.length != 32) { - _options.logger(SentryLevel.warning, - 'Hex input must be a 32-character hexadecimal string'); - return null; - } - - return '${hex.substring(0, 8)}-' - '${hex.substring(8, 12)}-' - '${hex.substring(12, 16)}-' - '${hex.substring(16, 20)}-' - '${hex.substring(20)}'; - } -} diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index 0932cac9af..6b9f9a6876 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -1,77 +1,129 @@ +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + import '../sentry.dart'; -import 'debug_image_extractor.dart'; class LoadDartDebugImagesIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { - options.addEventProcessor(_LoadImageIntegrationEventProcessor( - DebugImageExtractor(options), options)); + options.addEventProcessor(_LoadImageIntegrationEventProcessor(options)); options.sdk.addIntegration('loadDartImageIntegration'); } } -const hintRawStackTraceKey = 'raw_stacktrace'; - class _LoadImageIntegrationEventProcessor implements EventProcessor { - _LoadImageIntegrationEventProcessor(this._debugImageExtractor, this._options); + _LoadImageIntegrationEventProcessor(this._options); final SentryOptions _options; - final DebugImageExtractor _debugImageExtractor; + + // We don't need to always create the debug image, so we cache it here. + DebugImage? _debugImage; @override Future apply(SentryEvent event, Hint hint) async { - final rawStackTrace = hint.get(hintRawStackTraceKey) as String?; - if (!_options.enableDartSymbolication || - !event.needsSymbolication() || - rawStackTrace == null) { - return event; + if (_options.enableDartSymbolication) { + final stackTrace = event.stacktrace; + if (stackTrace!.frames.any((f) => f.platform == 'native')) { + try { + _debugImage ??= createDebugImage(stackTrace); + if (_debugImage != null) { + return event.copyWith(debugMeta: DebugMeta(images: [_debugImage!])); + } + } catch (e, stack) { + _options.logger( + SentryLevel.info, + "Couldn't add Dart debug image to event. " + 'The event will still be reported.', + exception: e, + stackTrace: stack, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } } - try { - final syntheticImage = _debugImageExtractor.extractFrom(rawStackTrace); - if (syntheticImage == null) { - return event; - } + return event; + } + + @visibleForTesting + DebugImage? createDebugImage(SentryStackTrace stackTrace) { + if (stackTrace.buildId == null || stackTrace.baseAddr == null) { + _options.logger(SentryLevel.warning, + 'Cannot create DebugImage without a build ID and image base address.'); + return null; + } - return event.copyWith(debugMeta: DebugMeta(images: [syntheticImage])); - } catch (e, stackTrace) { + late final String type; + late final String debugId; + + final platform = _options.platformChecker.platform; + + if (platform.isAndroid) { + type = 'elf'; + debugId = _convertBuildIdToDebugId(stackTrace.buildId!, platform.endian); + } else if (platform.isIOS || platform.isMacOS) { + type = 'macho'; + debugId = _formatHexToUuid(stackTrace.buildId!); + } else { _options.logger( - SentryLevel.info, - "Couldn't add Dart debug image to event. " - 'The event will still be reported.', - exception: e, - stackTrace: stackTrace, + SentryLevel.warning, + 'Unsupported platform for creating Dart debug images.', ); - if (_options.automatedTestMode) { - rethrow; - } - return event; + return null; } + + return DebugImage( + type: type, + imageAddr: stackTrace.baseAddr, + debugId: debugId, + codeId: stackTrace.buildId, + ); } -} -extension NeedsSymbolication on SentryEvent { - bool needsSymbolication() { - if (this is SentryTransaction) { - return false; - } - final frames = _getStacktraceFrames(); - if (frames == null) { - return false; + /// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 + /// Converts an ELF object identifier into a `DebugId`. + /// + /// The identifier data is first truncated or extended to match 16 byte size of + /// Uuids. If the data is declared in little endian, the first three Uuid fields + /// are flipped to match the big endian expected by the breakpad processor. + /// + /// The `DebugId::appendix` field is always `0` for ELF. + String _convertBuildIdToDebugId(String buildId, Endian endian) { + // Make sure that we have exactly UUID_SIZE bytes available + const uuidSize = 16 * 2; + final data = Uint8List(uuidSize); + final len = buildId.length.clamp(0, uuidSize); + data.setAll(0, buildId.codeUnits.take(len)); + + if (endian == Endian.little) { + // The file ELF file targets a little endian architecture. Convert to + // network byte order (big endian) to match the Breakpad processor's + // expectations. For big endian object files, this is not needed. + // To manipulate this as hex, we create an Uint16 view. + final data16 = Uint16List.view(data.buffer); + data16.setRange(0, 4, data16.sublist(0, 4).reversed); + data16.setRange(4, 6, data16.sublist(4, 6).reversed); + data16.setRange(6, 8, data16.sublist(6, 8).reversed); } - return frames.any((frame) => 'native' == frame?.platform); + return _formatHexToUuid(String.fromCharCodes(data)); } - Iterable? _getStacktraceFrames() { - if (exceptions?.isNotEmpty == true) { - return exceptions?.first.stackTrace?.frames; + String _formatHexToUuid(String hex) { + if (hex.length == 36) { + return hex; } - if (threads?.isNotEmpty == true) { - var stacktraces = threads?.map((e) => e.stacktrace); - return stacktraces - ?.where((element) => element != null) - .expand((element) => element!.frames); + if (hex.length != 32) { + throw ArgumentError.value(hex, 'hexUUID', + 'Hex input must be a 32-character hexadecimal string'); } - return null; + + return '${hex.substring(0, 8)}-' + '${hex.substring(8, 12)}-' + '${hex.substring(12, 16)}-' + '${hex.substring(16, 20)}-' + '${hex.substring(20)}'; } } diff --git a/dart/lib/src/platform/platform.dart b/dart/lib/src/platform/platform.dart index dffd3e81fd..ab2f94dd5f 100644 --- a/dart/lib/src/platform/platform.dart +++ b/dart/lib/src/platform/platform.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import '_io_platform.dart' if (dart.library.html) '_html_platform.dart' if (dart.library.js_interop) '_web_platform.dart' as platform; @@ -17,6 +19,9 @@ abstract class Platform { /// Get the local hostname for the system. String get localHostname; + /// Endianness of this platform. + Endian get endian => Endian.host; + /// True if the operating system is Linux. bool get isLinux => (operatingSystem == 'linux'); diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index 1b2765c426..fe7e0af47f 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -1,4 +1,5 @@ import 'package:meta/meta.dart'; +import 'package:collection/collection.dart'; import '../protocol.dart'; import '../throwable_mechanism.dart'; @@ -411,4 +412,10 @@ class SentryEvent with SentryEventLike { if (threadJson?.isNotEmpty ?? false) 'threads': {'values': threadJson}, }; } + + // Returns first non-null stack trace of this event + @internal + SentryStackTrace? get stacktrace => + exceptions?.firstWhereOrNull((e) => e.stackTrace != null)?.stackTrace ?? + threads?.firstWhereOrNull((t) => t.stacktrace != null)?.stacktrace; } diff --git a/dart/lib/src/protocol/sentry_stack_frame.dart b/dart/lib/src/protocol/sentry_stack_frame.dart index edba949e9f..84880cda5e 100644 --- a/dart/lib/src/protocol/sentry_stack_frame.dart +++ b/dart/lib/src/protocol/sentry_stack_frame.dart @@ -91,6 +91,7 @@ class SentryStackFrame { /// The "package" the frame was contained in. final String? package; + // TODO what is this? doesn't seem to be part of the spec https://develop.sentry.dev/sdk/event-payloads/stacktrace/ final bool? native; /// This can override the platform for a single frame. Otherwise, the platform of the event is assumed. This can be used for multi-platform stack traces diff --git a/dart/lib/src/protocol/sentry_stack_trace.dart b/dart/lib/src/protocol/sentry_stack_trace.dart index 8f229462dd..8aceaaf269 100644 --- a/dart/lib/src/protocol/sentry_stack_trace.dart +++ b/dart/lib/src/protocol/sentry_stack_trace.dart @@ -12,8 +12,8 @@ class SentryStackTrace { this.lang, this.snapshot, this.unknown, - @internal this.nativeImageBaseAddr, - @internal this.nativeBuildId, + @internal this.baseAddr, + @internal this.buildId, }) : _frames = frames, _registers = Map.from(registers ?? {}); @@ -49,10 +49,10 @@ class SentryStackTrace { final bool? snapshot; @internal - final String? nativeImageBaseAddr; + final String? baseAddr; @internal - final String? nativeBuildId; + final String? buildId; @internal final Map? unknown; @@ -99,7 +99,7 @@ class SentryStackTrace { lang: lang ?? this.lang, snapshot: snapshot ?? this.snapshot, unknown: unknown, - nativeImageBaseAddr: nativeImageBaseAddr, - nativeBuildId: nativeBuildId, + baseAddr: baseAddr, + buildId: buildId, ); } diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index bc54651d53..69b838c817 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -7,7 +7,6 @@ import 'client_reports/client_report_recorder.dart'; import 'client_reports/discard_reason.dart'; import 'event_processor.dart'; import 'hint.dart'; -import 'load_dart_debug_images_integration.dart'; import 'metrics/metric.dart'; import 'metrics/metrics_aggregator.dart'; import 'protocol.dart'; @@ -119,7 +118,6 @@ class SentryClient { SentryEvent? preparedEvent = _prepareEvent(event, stackTrace: stackTrace); hint ??= Hint(); - hint.set(hintRawStackTraceKey, stackTrace.toString()); if (scope != null) { preparedEvent = await scope.applyToEvent(preparedEvent, hint); diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index c9a9511c29..95b3f225ab 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -366,8 +366,8 @@ class SentryOptions { /// [Sentry.init] is used instead of `SentryFlutter.init`. This is useful /// when native debug images are not available. /// - /// Automatically set to `false` when using `SentryFlutter.init`, as it uses - /// native SDKs for setting up symbolication on iOS, macOS, and Android. + /// Automatically set to `false` when using `SentryFlutter.init` on a platform + /// with a native integration (e.g. Android, iOS, ...x). bool enableDartSymbolication = true; @internal diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index 994e4be479..fa3c2ec321 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -11,8 +11,8 @@ class SentryStackTraceFactory { static final _absRegex = RegExp(r'^\s*#[0-9]+ +abs +([A-Fa-f0-9]+)'); static final _frameRegex = RegExp(r'^\s*#', multiLine: true); - static final _buildIdRegex = RegExp(r"build_id: '([A-Fa-f0-9]+)'"); - static final _baseAddrRegex = RegExp(r'vm_dso_base: ([A-Fa-f0-9]+)'); + static final _buildIdRegex = RegExp(r"build_id[:=] *'([A-Fa-f0-9]+)'"); + static final _baseAddrRegex = RegExp(r'isolate_dso_base[:=] *([A-Fa-f0-9]+)'); static final SentryStackFrame _asynchronousGapFrameJson = SentryStackFrame(absPath: ''); @@ -50,8 +50,8 @@ class SentryStackTraceFactory { return SentryStackTrace( frames: onlyAsyncGap ? [] : frames.reversed.toList(), - nativeImageBaseAddr: parsed.imageBaseAddr, - nativeBuildId: parsed.buildId, + baseAddr: parsed.baseAddr, + buildId: parsed.buildId, ); } @@ -84,20 +84,10 @@ class SentryStackTraceFactory { final chain = Chain.parse( startOffset == 0 ? stackTrace : stackTrace.substring(startOffset)); final info = _StackInfo(chain.traces); - - // On Windows native, we need to provide the debug image address so that - // the LoadImageList integration can add a virtual image. - // This is because on Windows, dart uses ELF binaries for AOT code instead - // of standard windows-specific PE and it has a custom ELF image loader - // that loaads the "data/App.so". Therefore, sentry-native is unaware - // of this and won't pick list the image among native images. - // See https://github.com/flutter/flutter/issues/154840 - if (_options.platformChecker.platform.isWindows) { - info.buildId = _buildIdRegex.firstMatch(stackTrace)?.group(1); - info.imageBaseAddr = _baseAddrRegex.firstMatch(stackTrace)?.group(1); - if (info.imageBaseAddr != null) { - info.imageBaseAddr = '0x${info.imageBaseAddr}'; - } + info.buildId = _buildIdRegex.firstMatch(stackTrace)?.group(1); + info.baseAddr = _baseAddrRegex.firstMatch(stackTrace)?.group(1); + if (info.baseAddr != null) { + info.baseAddr = '0x${info.baseAddr}'; } return info; } @@ -126,6 +116,8 @@ class SentryStackTraceFactory { // We shouldn't get here. If we do, it means there's likely an issue in // the parsing so let's fall back and post a stack trace as is, so that at // least we get an indication something's wrong and are able to fix it. + _options.logger( + SentryLevel.debug, "Failed to parse stack frame: $member"); } final platform = _options.platformChecker.isWeb ? 'javascript' : 'dart'; @@ -209,7 +201,7 @@ class SentryStackTraceFactory { } class _StackInfo { - String? imageBaseAddr; + String? baseAddr; String? buildId; final List traces; diff --git a/dart/test/debug_image_extractor_test.dart b/dart/test/debug_image_extractor_test.dart deleted file mode 100644 index 7a0ad7d12f..0000000000 --- a/dart/test/debug_image_extractor_test.dart +++ /dev/null @@ -1,118 +0,0 @@ -import 'package:test/test.dart'; -import 'package:sentry/src/debug_image_extractor.dart'; - -import 'mocks/mock_platform.dart'; -import 'mocks/mock_platform_checker.dart'; -import 'test_utils.dart'; - -void main() { - group(DebugImageExtractor, () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('returns null for invalid stack trace', () { - final stackTrace = 'Invalid stack trace'; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect(debugImage, isNull); - }); - - test('extracts correct debug ID for Android with short debugId', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 20000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('89cb80b6-9e0f-123c-a24b-172d050dec73')); - }); - - test('extracts correct debug ID for Android with long debugId', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'f1c3bcc0279865fe3058404b2831d9e64135386c' -isolate_dso_base: 30000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('c0bcc3f1-9827-fe65-3058-404b2831d9e6')); - }); - - test('extracts correct debug ID for iOS', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 30000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.iOS()); - final debugImage = extractor.extractFrom(stackTrace); - - expect( - debugImage?.debugId, equals('b680cb89-0f9e-3c12-a24b-172d050dec73')); - expect(debugImage?.codeId, isNull); - }); - - test('sets correct type based on platform', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 40000000 -'''; - final androidExtractor = fixture.getSut(platform: MockPlatform.android()); - final iosExtractor = fixture.getSut(platform: MockPlatform.iOS()); - - final androidDebugImage = androidExtractor.extractFrom(stackTrace); - final iosDebugImage = iosExtractor.extractFrom(stackTrace); - - expect(androidDebugImage?.type, equals('elf')); - expect(iosDebugImage?.type, equals('macho')); - }); - - test('debug image is null on unsupported platforms', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 40000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.linux()); - - final debugImage = extractor.extractFrom(stackTrace); - - expect(debugImage, isNull); - }); - - test('debugImage is cached after first extraction', () { - final stackTrace = ''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 10000000 -'''; - final extractor = fixture.getSut(platform: MockPlatform.android()); - - // First extraction - final debugImage1 = extractor.extractFrom(stackTrace); - expect(debugImage1, isNotNull); - expect(extractor.debugImageForTesting, equals(debugImage1)); - - // Second extraction - final debugImage2 = extractor.extractFrom(stackTrace); - expect(debugImage2, equals(debugImage1)); - }); - }); -} - -class Fixture { - DebugImageExtractor getSut({required MockPlatform platform}) { - final options = defaultTestOptions(MockPlatformChecker(platform: platform)); - return DebugImageExtractor(options); - } -} diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index e7f06525ce..4a463b3d1b 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -1,8 +1,11 @@ @TestOn('vm') library dart_test; +import 'dart:async'; + import 'package:sentry/sentry.dart'; import 'package:sentry/src/load_dart_debug_images_integration.dart'; +import 'package:sentry/src/sentry_stack_trace_factory.dart'; import 'package:test/test.dart'; import 'mocks/mock_platform.dart'; @@ -10,16 +13,16 @@ import 'mocks/mock_platform_checker.dart'; import 'test_utils.dart'; void main() { - group(LoadDartDebugImagesIntegration, () { - late Fixture fixture; + final platforms = [ + MockPlatform.iOS(), + MockPlatform.macOS(), + MockPlatform.android(), + ]; - final platforms = [ - MockPlatform.iOS(), - MockPlatform.macOS(), - MockPlatform.android(), - ]; + for (final platform in platforms) { + group('$LoadDartDebugImagesIntegration $platform', () { + late Fixture fixture; - for (final platform in platforms) { setUp(() { fixture = Fixture(); fixture.options.platformChecker = @@ -44,18 +47,16 @@ void main() { test( 'Event processor does not add debug image if symbolication is not needed', () async { - final event = _getEvent(needsSymbolication: false); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(needsSymbolication: false); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); test('Event processor does not add debug image if stackTrace is null', () async { - final event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); @@ -64,47 +65,133 @@ void main() { 'Event processor does not add debug image if enableDartSymbolication is false', () async { fixture.options.enableDartSymbolication = false; - final event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply(event, Hint()); + final event = fixture.newEvent(); + final resultEvent = await fixture.process(event); expect(resultEvent, equals(event)); }); test('Event processor adds debug image when symbolication is needed', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + expect(debugImage?.debugId, isNotEmpty); + expect(debugImage?.imageAddr, equals('0x10000000')); + }); + + test('returns null for invalid stack trace', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + expect(debugImage, isNull); + }); + + test('extracts correct debug ID with short debugId', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 20000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + expect(debugImage?.debugId, + equals('89cb80b6-9e0f-123c-a24b-172d050dec73')); + }); + + test('extracts correct debug ID for Android with long debugId', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'f1c3bcc0279865fe3058404b2831d9e64135386c' +isolate_dso_base: 30000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + expect(debugImage?.debugId, + equals('c0bcc3f1-9827-fe65-3058-404b2831d9e6')); + }); + + test('sets correct type based on platform', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + if (platform.isAndroid) { + expect(debugImage?.type, 'elf'); + } else if (platform.isIOS || platform.isMacOS) { + expect(debugImage?.type, 'macho'); + } + }); + + test('debugImage is cached after first extraction', () async { final stackTrace = ''' *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** build_id: 'b680cb890f9e3c12a24b172d050dec73' isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 '''; - SentryEvent event = _getEvent(); - final processor = fixture.options.eventProcessors.first; - final resultEvent = await processor.apply( - event, Hint()..set(hintRawStackTraceKey, stackTrace)); + // First extraction + final debugImage1 = await fixture.parseAndProcess(stackTrace); + expect(debugImage1, isNotNull); - expect(resultEvent?.debugMeta?.images.length, 1); - final debugImage = resultEvent?.debugMeta?.images.first; - expect(debugImage?.debugId, isNotEmpty); - expect(debugImage?.imageAddr, equals('0x10000000')); + // Second extraction + final debugImage2 = await fixture.parseAndProcess(stackTrace); + expect(debugImage2, equals(debugImage1)); }); - } + }); + } + + test('debug image is null on unsupported platforms', () async { + final fixture = Fixture() + ..options.platformChecker = + MockPlatformChecker(platform: MockPlatform.linux()); + final event = fixture.newEvent(stackTrace: fixture.parse(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +''')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images.length, 0); }); } class Fixture { final options = defaultTestOptions(); + late final factory = SentryStackTraceFactory(options); Fixture() { final integration = LoadDartDebugImagesIntegration(); integration.call(Hub(options), options); } -} -SentryEvent _getEvent({bool needsSymbolication = true}) { - final frame = - SentryStackFrame(platform: needsSymbolication ? 'native' : 'dart'); - final st = SentryStackTrace(frames: [frame]); - return SentryEvent( - threads: [SentryThread(stacktrace: st)], debugMeta: DebugMeta()); + SentryStackTrace parse(String stacktrace) => factory.create(stacktrace); + + SentryEvent newEvent( + {bool needsSymbolication = true, SentryStackTrace? stackTrace}) { + stackTrace ??= SentryStackTrace(frames: [ + SentryStackFrame(platform: needsSymbolication ? null : 'dart') + ]); + return SentryEvent( + threads: [SentryThread(stacktrace: stackTrace)], + debugMeta: DebugMeta()); + } + + FutureOr process(SentryEvent event) => + options.eventProcessors.first.apply(event, Hint()); + + Future parseAndProcess(String stacktrace) async { + final event = newEvent(stackTrace: parse(stacktrace)); + final resultEvent = await process(event); + expect(resultEvent?.debugMeta?.images.length, 1); + return resultEvent?.debugMeta?.images.first; + } } diff --git a/dart/test/mocks/mock_platform.dart b/dart/test/mocks/mock_platform.dart index 75025a4a15..21dc234b09 100644 --- a/dart/test/mocks/mock_platform.dart +++ b/dart/test/mocks/mock_platform.dart @@ -1,9 +1,13 @@ +import 'dart:typed_data'; + import 'package:sentry/src/platform/platform.dart'; import 'no_such_method_provider.dart'; class MockPlatform extends Platform with NoSuchMethodProvider { - MockPlatform({String? os}) : operatingSystem = os ?? ''; + MockPlatform({String? os, Endian? endian}) + : operatingSystem = os ?? '', + endian = endian ?? Endian.host; factory MockPlatform.android() { return MockPlatform(os: 'android'); @@ -26,5 +30,11 @@ class MockPlatform extends Platform with NoSuchMethodProvider { } @override - String operatingSystem; + final String operatingSystem; + + @override + final Endian endian; + + @override + String toString() => operatingSystem; } diff --git a/dart/test/sentry_exception_factory_test.dart b/dart/test/sentry_exception_factory_test.dart index a592bf78b9..f2e12fce10 100644 --- a/dart/test/sentry_exception_factory_test.dart +++ b/dart/test/sentry_exception_factory_test.dart @@ -2,12 +2,11 @@ import 'package:sentry/sentry.dart'; import 'package:sentry/src/sentry_exception_factory.dart'; import 'package:test/test.dart'; -import 'mocks/mock_platform.dart'; -import 'mocks/mock_platform_checker.dart'; import 'test_utils.dart'; void main() { late Fixture fixture; + setUp(() { fixture = Fixture(); }); @@ -212,16 +211,24 @@ void main() { expect(sentryException.stackTrace!.snapshot, true); }); - test('sets native image start address on Windows', () { - fixture.options.platformChecker = - MockPlatformChecker(platform: MockPlatform.windows()); + test('sets stacktrace build id and image address', () { final sentryException = fixture .getSut(attachStacktrace: false) .getSentryException(Object(), stackTrace: StackTraceErrorStackTrace()); final sentryStackTrace = sentryException.stackTrace!; - expect(sentryStackTrace.nativeImageBaseAddr, '0x752602b000'); - expect(sentryStackTrace.nativeBuildId, 'bca64abfdfcc84d231bb8f1ccdbfbd8d'); + expect(sentryStackTrace.baseAddr, '0x752602b000'); + expect(sentryStackTrace.buildId, 'bca64abfdfcc84d231bb8f1ccdbfbd8d'); + }); + + test('sets null build id and image address if not present', () { + final sentryException = fixture + .getSut(attachStacktrace: false) + .getSentryException(Object(), stackTrace: null); + + final sentryStackTrace = sentryException.stackTrace!; + expect(sentryStackTrace.baseAddr, isNull); + expect(sentryStackTrace.buildId, isNull); }); } diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 891f2e6333..b9e73a6a04 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -22,16 +22,6 @@ class LoadImageListIntegration extends Integration { } } -extension on SentryEvent { - SentryStackTrace? _getStacktrace() { - var stackTrace = - exceptions?.firstWhereOrNull((e) => e.stackTrace != null)?.stackTrace; - stackTrace ??= - threads?.firstWhereOrNull((t) => t.stacktrace != null)?.stacktrace; - return stackTrace; - } -} - class _LoadImageListIntegrationEventProcessor implements EventProcessor { _LoadImageListIntegrationEventProcessor(this._native); @@ -39,7 +29,7 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, Hint hint) async { - final stackTrace = event._getStacktrace(); + final stackTrace = event.stacktrace; // if the stacktrace has native frames, we load native debug images. if (stackTrace != null && diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 9b60a15d62..b531570add 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -240,9 +240,9 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { Future getAppDebugImage( SentryStackTrace stackTrace, Iterable nativeImages) async { // ignore: invalid_use_of_internal_member - final buildId = stackTrace.nativeBuildId; + final buildId = stackTrace.buildId; // ignore: invalid_use_of_internal_member - final imageAddr = stackTrace.nativeImageBaseAddr; + final imageAddr = stackTrace.baseAddr; if (buildId == null || imageAddr == null) { return null; diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index 2e6d173277..fbfd0ca903 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -240,9 +240,9 @@ void main() { SentryStackTrace( frames: [], // ignore: invalid_use_of_internal_member - nativeBuildId: '4c6950bd9e9cc9839071742a7295c09e', + buildId: '4c6950bd9e9cc9839071742a7295c09e', // ignore: invalid_use_of_internal_member - nativeImageBaseAddr: '0x123', + baseAddr: '0x123', ), [DebugImage(type: 'pe', codeFile: '/path/to/application.exe')], ); From d2ee5ac131d711124df08796db026eadee71a6cd Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 11:21:27 +0200 Subject: [PATCH 32/55] fix debug image integration and tests --- .../load_dart_debug_images_integration.dart | 18 +++++++++++++++- ...ad_dart_debug_images_integration_test.dart | 21 ++++++++++--------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index 6b9f9a6876..41b3ecba3a 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -28,7 +28,15 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor { try { _debugImage ??= createDebugImage(stackTrace); if (_debugImage != null) { - return event.copyWith(debugMeta: DebugMeta(images: [_debugImage!])); + late final DebugMeta debugMeta; + if (event.debugMeta != null) { + final images = List.from(event.debugMeta!.images); + images.add(_debugImage!); + debugMeta = event.debugMeta!.copyWith(images: images); + } else { + debugMeta = DebugMeta(images: [_debugImage!]); + } + return event.copyWith(debugMeta: debugMeta); } } catch (e, stack) { _options.logger( @@ -56,17 +64,24 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor { return null; } + // Type and DebugID are required for proper symbolication late final String type; late final String debugId; + // CodeFile is required so that the debug image shows up properly in the UI. + // It doesn't need to exist and is not used for symbolication. + late final String codeFile; + final platform = _options.platformChecker.platform; if (platform.isAndroid) { type = 'elf'; debugId = _convertBuildIdToDebugId(stackTrace.buildId!, platform.endian); + codeFile = 'libapp.so'; } else if (platform.isIOS || platform.isMacOS) { type = 'macho'; debugId = _formatHexToUuid(stackTrace.buildId!); + codeFile = 'App.Framework/App'; } else { _options.logger( SentryLevel.warning, @@ -80,6 +95,7 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor { imageAddr: stackTrace.baseAddr, debugId: debugId, codeId: stackTrace.buildId, + codeFile: codeFile, ); } diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index 4a463b3d1b..6466a6e245 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -84,13 +84,10 @@ isolate_dso_base: 10000000 }); test('returns null for invalid stack trace', () async { - final debugImage = await fixture.parseAndProcess(''' -*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** -build_id: 'b680cb890f9e3c12a24b172d050dec73' -isolate_dso_base: 10000000 - #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 -'''); - expect(debugImage, isNull); + final event = + fixture.newEvent(stackTrace: fixture.parse('Invalid stack trace')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images, isEmpty); }); test('extracts correct debug ID with short debugId', () async { @@ -100,8 +97,12 @@ build_id: 'b680cb890f9e3c12a24b172d050dec73' isolate_dso_base: 20000000 #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 '''); - expect(debugImage?.debugId, - equals('89cb80b6-9e0f-123c-a24b-172d050dec73')); + + if (platform.isAndroid) { + expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73'); + } else { + expect(debugImage?.debugId, 'b680cb89-0f9e-3c12-a24b-172d050dec73'); + } }); test('extracts correct debug ID for Android with long debugId', () async { @@ -114,7 +115,7 @@ isolate_dso_base: 30000000 expect(debugImage?.debugId, equals('c0bcc3f1-9827-fe65-3058-404b2831d9e6')); - }); + }, skip: !platform.isAndroid); test('sets correct type based on platform', () async { final debugImage = await fixture.parseAndProcess(''' From 1c7b63d535db6a4221f6c8f7211fb1f45cc41b6b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 11:21:39 +0200 Subject: [PATCH 33/55] tmp: use sentry.init in the flutter example --- flutter/example/lib/main.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index f34a1f5a7f..1967827f02 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -62,36 +62,36 @@ Future setupSentry( bool isIntegrationTest = false, BeforeSendCallback? beforeSendCallback, }) async { - await SentryFlutter.init( + await Sentry.init( (options) { options.dsn = exampleDsn; options.tracesSampleRate = 1.0; - options.profilesSampleRate = 1.0; - options.reportPackages = false; + // options.profilesSampleRate = 1.0; + // options.reportPackages = false; options.addInAppInclude('sentry_flutter_example'); options.considerInAppFramesByDefault = false; options.attachThreads = true; - options.enableWindowMetricBreadcrumbs = true; + // options.enableWindowMetricBreadcrumbs = true; options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); options.sendDefaultPii = true; - options.reportSilentFlutterErrors = true; - options.attachScreenshot = true; - options.screenshotQuality = SentryScreenshotQuality.low; - options.attachViewHierarchy = true; + // options.reportSilentFlutterErrors = true; + // options.attachScreenshot = true; + // options.screenshotQuality = SentryScreenshotQuality.low; + // options.attachViewHierarchy = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; options.spotlight = Spotlight(enabled: true); - options.enableTimeToFullDisplayTracing = true; + // options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; - options.navigatorKey = navigatorKey; + // options.navigatorKey = navigatorKey; - options.experimental.replay.sessionSampleRate = 1.0; - options.experimental.replay.onErrorSampleRate = 1.0; + // options.experimental.replay.sessionSampleRate = 1.0; + // options.experimental.replay.onErrorSampleRate = 1.0; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { From c4178c7964c81f7981b979303f8a3523d3f100ed Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 11:56:56 +0200 Subject: [PATCH 34/55] fixup dart deps --- dart/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/pubspec.yaml b/dart/pubspec.yaml index 6141a2eba6..46b4a52923 100644 --- a/dart/pubspec.yaml +++ b/dart/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: meta: ^1.3.0 stack_trace: ^1.10.0 uuid: '>=3.0.0 <5.0.0' + collection: ^1.16.0 dev_dependencies: build_runner: ^2.3.0 @@ -31,7 +32,6 @@ dev_dependencies: lints: '>=2.0.0 <5.0.0' test: ^1.21.1 yaml: ^3.1.0 # needed for version match (code and pubspec) - collection: ^1.16.0 coverage: ^1.3.0 intl: '>=0.17.0 <1.0.0' version: ^3.0.2 From 490c049d0cde1ea7713b17600385ac1b583440f3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 11:58:27 +0200 Subject: [PATCH 35/55] comment fix --- dart/lib/src/sentry_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/src/sentry_options.dart b/dart/lib/src/sentry_options.dart index 95b3f225ab..83fb887124 100644 --- a/dart/lib/src/sentry_options.dart +++ b/dart/lib/src/sentry_options.dart @@ -367,7 +367,7 @@ class SentryOptions { /// when native debug images are not available. /// /// Automatically set to `false` when using `SentryFlutter.init` on a platform - /// with a native integration (e.g. Android, iOS, ...x). + /// with a native integration (e.g. Android, iOS, ...). bool enableDartSymbolication = true; @internal From 1d648a3a7d1326600531847b1fa4e1d2a041fc84 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 12:00:51 +0200 Subject: [PATCH 36/55] rename: SentryStackTraceFactory create() to parse() --- dart/lib/src/sentry_client.dart | 2 +- dart/lib/src/sentry_exception_factory.dart | 2 +- dart/lib/src/sentry_stack_trace_factory.dart | 4 ++-- dart/test/load_dart_debug_images_integration_test.dart | 2 +- dart/test/sentry_client_test.dart | 2 +- dart/test/stack_trace_test.dart | 10 +++++----- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 69b838c817..6358b13012 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -269,7 +269,7 @@ class SentryClient { // https://develop.sentry.dev/sdk/event-payloads/stacktrace/ if (stackTrace != null || _options.attachStacktrace) { stackTrace ??= getCurrentStackTrace(); - final sentryStackTrace = _stackTraceFactory.create(stackTrace); + final sentryStackTrace = _stackTraceFactory.parse(stackTrace); if (sentryStackTrace.frames.isNotEmpty) { event = event.copyWith(threads: [ ...?event.threads, diff --git a/dart/lib/src/sentry_exception_factory.dart b/dart/lib/src/sentry_exception_factory.dart index e019b45e94..6f766014ac 100644 --- a/dart/lib/src/sentry_exception_factory.dart +++ b/dart/lib/src/sentry_exception_factory.dart @@ -48,7 +48,7 @@ class SentryExceptionFactory { SentryStackTrace? sentryStackTrace; if (stackTrace != null) { sentryStackTrace = - _stacktraceFactory.create(stackTrace).copyWith(snapshot: snapshot); + _stacktraceFactory.parse(stackTrace).copyWith(snapshot: snapshot); if (sentryStackTrace.frames.isEmpty) { sentryStackTrace = null; } diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index fa3c2ec321..e3ff4aabad 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -21,10 +21,10 @@ class SentryStackTraceFactory { /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) @deprecated List getStackFrames(dynamic stackTrace) { - return create(stackTrace).frames; + return parse(stackTrace).frames; } - SentryStackTrace create(dynamic stackTrace) { + SentryStackTrace parse(dynamic stackTrace) { final parsed = _parseStackTrace(stackTrace); final frames = []; var onlyAsyncGap = true; diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index 6466a6e245..2ea5989393 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -174,7 +174,7 @@ class Fixture { integration.call(Hub(options), options); } - SentryStackTrace parse(String stacktrace) => factory.create(stacktrace); + SentryStackTrace parse(String stacktrace) => factory.parse(stacktrace); SentryEvent newEvent( {bool needsSymbolication = true, SentryStackTrace? stackTrace}) { diff --git a/dart/test/sentry_client_test.dart b/dart/test/sentry_client_test.dart index 7109ddea32..5e5d00fddc 100644 --- a/dart/test/sentry_client_test.dart +++ b/dart/test/sentry_client_test.dart @@ -99,7 +99,7 @@ void main() { type: 'Exception', value: 'an exception', stackTrace: SentryStackTraceFactory(fixture.options) - .create('#0 baz (file:///pathto/test.dart:50:3)'), + .parse('#0 baz (file:///pathto/test.dart:50:3)'), ); final event = SentryEvent(exceptions: [exception]); diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index 3e8f6b9f4e..5b65359be4 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -111,7 +111,7 @@ void main() { group('encodeStackTrace', () { test('encodes a simple stack trace', () { final frames = - Fixture().getSut(considerInAppFramesByDefault: true).create(''' + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) ''').frames.map((frame) => frame.toJson()); @@ -140,7 +140,7 @@ void main() { test('encodes an asynchronous stack trace', () { final frames = - Fixture().getSut(considerInAppFramesByDefault: true).create(''' + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 baz (file:///pathto/test.dart:50:3) #1 bar (file:///pathto/test.dart:46:9) @@ -199,7 +199,7 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 for (var traceString in stackTraces) { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .create(traceString) + .parse(traceString) .frames .map((frame) => frame.toJson()); @@ -221,7 +221,7 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 test('parses normal stack trace', () { final frames = - Fixture().getSut(considerInAppFramesByDefault: true).create(''' + Fixture().getSut(considerInAppFramesByDefault: true).parse(''' #0 asyncThrows (file:/foo/bar/main.dart:404) #1 MainScaffold.build. (package:example/main.dart:131) #2 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:341) @@ -258,7 +258,7 @@ isolate_instructions: 10fa27070, vm_instructions: 10fa21e20 test('remove frames if only async gap is left', () { final frames = Fixture() .getSut(considerInAppFramesByDefault: true) - .create(StackTrace.fromString(''' + .parse(StackTrace.fromString(''' ''')) .frames From b5dcd2a7ce0a7f34c3fd8feb8f0aa8b6bb7868c9 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 16:18:03 +0200 Subject: [PATCH 37/55] linter isssues --- flutter/lib/src/integrations/load_image_list_integration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index b9e73a6a04..191c06faae 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:sentry/sentry.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; @@ -29,6 +28,7 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, Hint hint) async { + // ignore: invalid_use_of_internal_member final stackTrace = event.stacktrace; // if the stacktrace has native frames, we load native debug images. From 3f85fc1a49e3a7b2a4c43269514e0e63dfdff928 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 17:03:51 +0200 Subject: [PATCH 38/55] analyzer issue --- dart/lib/src/sentry_stack_trace_factory.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dart/lib/src/sentry_stack_trace_factory.dart b/dart/lib/src/sentry_stack_trace_factory.dart index e3ff4aabad..efcca49c70 100644 --- a/dart/lib/src/sentry_stack_trace_factory.dart +++ b/dart/lib/src/sentry_stack_trace_factory.dart @@ -19,7 +19,7 @@ class SentryStackTraceFactory { SentryStackTraceFactory(this._options); /// returns the [SentryStackFrame] list from a stackTrace ([StackTrace] or [String]) - @deprecated + @Deprecated('Use parse() instead') List getStackFrames(dynamic stackTrace) { return parse(stackTrace).frames; } From 607b3827f7758c9535ff69e61223a3726405bf20 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 20:04:46 +0200 Subject: [PATCH 39/55] fix tests on web --- .../sentry_native/sentry_native_test.dart | 334 +----------------- .../sentry_native/sentry_native_test_ffi.dart | 326 +++++++++++++++++ .../sentry_native/sentry_native_test_web.dart | 1 + 3 files changed, 337 insertions(+), 324 deletions(-) create mode 100644 flutter/test/sentry_native/sentry_native_test_ffi.dart create mode 100644 flutter/test/sentry_native/sentry_native_test_web.dart diff --git a/flutter/test/sentry_native/sentry_native_test.dart b/flutter/test/sentry_native/sentry_native_test.dart index fbfd0ca903..3b720df7af 100644 --- a/flutter/test/sentry_native/sentry_native_test.dart +++ b/flutter/test/sentry_native/sentry_native_test.dart @@ -1,329 +1,15 @@ +// We must conditionally import the actual test code, otherwise tests fail on +// a browser. @TestOn('vm') doesn't help by itself in this case because imports +// are still evaluated, thus causing a compilation failure. @TestOn('vm && windows') -library flutter_test; +library sentry_native_test; -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:ffi/ffi.dart'; -import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:sentry/src/platform/platform.dart' as platform; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/native/c/sentry_native.dart'; -import 'package:sentry_flutter/src/native/factory.dart'; - -import '../mocks.dart'; -import '../mocks.mocks.dart'; - -late final String repoRootDir; -late final List expectedDistFiles; - -void main() { - repoRootDir = Directory.current.path.endsWith('/test') - ? Directory.current.parent.path - : Directory.current.path; - - expectedDistFiles = [ - 'sentry.dll', - 'crashpad_handler.exe', - 'crashpad_wer.dll', - ]; - - setUpAll(() async { - Directory.current = - await _buildSentryNative('$repoRootDir/temp/native-test'); - SentryNative.crashpadPath = - '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.startsWith('crashpad_handler'))}'; - }); - - late SentryNative sut; - late SentryFlutterOptions options; - - setUp(() { - options = SentryFlutterOptions(dsn: fakeDsn) - // ignore: invalid_use_of_internal_member - ..automatedTestMode = true - ..debug = true - ..fileSystem = MemoryFileSystem.test(); - sut = createBinding(options) as SentryNative; - }); - - test('expected output files', () { - for (var name in expectedDistFiles) { - if (!File(name).existsSync()) { - fail('Native distribution file $name does not exist'); - } - } - }); - - test('options', () { - options - ..debug = true - ..environment = 'foo' - ..release = 'foo@bar+1' - ..enableAutoSessionTracking = true - ..dist = 'distfoo' - ..maxBreadcrumbs = 42; - - final cOptions = sut.createOptions(options); - try { - expect( - SentryNative.native - .options_get_dsn(cOptions) - .cast() - .toDartString(), - fakeDsn); - expect( - SentryNative.native - .options_get_environment(cOptions) - .cast() - .toDartString(), - 'foo'); - expect( - SentryNative.native - .options_get_release(cOptions) - .cast() - .toDartString(), - 'foo@bar+1'); - expect( - SentryNative.native.options_get_auto_session_tracking(cOptions), 1); - expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); - } finally { - SentryNative.native.options_free(cOptions); - } - }); - - test('SDK version', () { - expect(_configuredSentryNativeVersion.length, greaterThanOrEqualTo(5)); - expect(SentryNative.native.sdk_version().cast().toDartString(), - _configuredSentryNativeVersion); - }); - - test('SDK name', () { - expect(SentryNative.native.sdk_name().cast().toDartString(), - 'sentry.native.flutter'); - }); - - test('init', () async { - addTearDown(sut.close); - await sut.init(MockHub()); - }); - - test('app start', () { - expect(sut.fetchNativeAppStart(), null); - }); - - test('frames tracking', () { - sut.beginNativeFrames(); - expect(sut.endNativeFrames(SentryId.newId()), null); - }); - - test('hang tracking', () { - sut.pauseAppHangTracking(); - sut.resumeAppHangTracking(); - }); - - test('setUser', () async { - final user = SentryUser( - id: "fixture-id", - username: 'username', - email: 'mail@domain.tld', - ipAddress: '1.2.3.4', - name: 'User Name', - data: { - 'str': 'foo-bar', - 'double': 1.0, - 'int': 1, - 'int64': 0x7FFFFFFF + 1, - 'boo': true, - 'inner-map': {'str': 'inner'}, - 'unsupported': Object() - }, - ); - - await sut.setUser(user); - }); - - test('addBreadcrumb', () async { - final breadcrumb = Breadcrumb( - type: 'type', - message: 'message', - category: 'category', - ); - await sut.addBreadcrumb(breadcrumb); - }); - - test('clearBreadcrumbs', () async { - await sut.clearBreadcrumbs(); - }); - - test('displayRefreshRate', () async { - expect(sut.displayRefreshRate(), isNull); - }); - - test('setContexts', () async { - final value = {'object': Object()}; - await sut.setContexts('fixture-key', value); - }); - - test('removeContexts', () async { - await sut.removeContexts('fixture-key'); - }); - - test('setExtra', () async { - final value = {'object': Object()}; - await sut.setExtra('fixture-key', value); - }); - - test('removeExtra', () async { - await sut.removeExtra('fixture-key'); - }); - - test('setTag', () async { - await sut.setTag('fixture-key', 'fixture-value'); - }); - - test('removeTag', () async { - await sut.removeTag('fixture-key'); - }); - - test('startProfiler', () { - expect(() => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); - }); - - test('discardProfiler', () async { - expect(() => sut.discardProfiler(SentryId.newId()), throwsUnsupportedError); - }); - - test('collectProfile', () async { - final traceId = SentryId.newId(); - const startTime = 42; - const endTime = 50; - expect(() => sut.collectProfile(traceId, startTime, endTime), - throwsUnsupportedError); - }); - - test('captureEnvelope', () async { - final data = Uint8List.fromList([1, 2, 3]); - expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); - }); - - test('loadContexts', () async { - expect(await sut.loadContexts(), isNull); - }); - - test('loadDebugImages', () async { - final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); - expect(list, isNotEmpty); - expect(list![0].type, 'pe'); - expect(list[0].debugId!.length, greaterThan(30)); - expect(list[0].debugFile, isNotEmpty); - expect(list[0].imageSize, greaterThan(0)); - expect(list[0].imageAddr, startsWith('0x')); - expect(list[0].imageAddr?.length, greaterThan(2)); - expect(list[0].codeId!.length, greaterThan(10)); - expect(list[0].codeFile, isNotEmpty); - expect( - File(list[0].codeFile!), - (File file) => file.existsSync(), - ); - }); - - test('getAppDebugImage returns app.so debug image', () async { - await options.fileSystem.directory('/path/to/data').create(recursive: true); - await options.fileSystem - .file('/path/to/data/app.so') - .writeAsString('12345'); - - final image = await sut.getAppDebugImage( - SentryStackTrace( - frames: [], - // ignore: invalid_use_of_internal_member - buildId: '4c6950bd9e9cc9839071742a7295c09e', - // ignore: invalid_use_of_internal_member - baseAddr: '0x123', - ), - [DebugImage(type: 'pe', codeFile: '/path/to/application.exe')], - ); - - expect(image, isNotNull); - expect(image!.codeFile, '/path/to/data/app.so'); - expect(image.codeId, '4c6950bd9e9cc9839071742a7295c09e'); - expect(image.debugId, 'bd50694c9c9e83c99071742a7295c09e'); - expect(image.imageAddr, '0x123'); - expect(image.imageSize, 5); - }); -} - -/// Runs [command] with command's stdout and stderr being forwrarded to -/// test runner's respective streams. It buffers stdout and returns it. -/// -/// Returns [_CommandResult] with exitCode and stdout as a single sting -Future _exec(String executable, List arguments) async { - final process = await Process.start(executable, arguments); - - // forward standard streams - unawaited(stderr.addStream(process.stderr)); - unawaited(stdout.addStream(process.stdout)); - - int exitCode = await process.exitCode; - if (exitCode != 0) { - throw Exception( - "$executable ${arguments.join(' ')} failed with exit code $exitCode"); - } -} - -/// Compile sentry-native using CMake, as if it was part of a Flutter app. -/// Returns the directory containing built libraries -Future _buildSentryNative(String nativeTestRoot) async { - final cmakeBuildDir = '$nativeTestRoot/build'; - final cmakeConfDir = '$nativeTestRoot/conf'; - final buildOutputDir = '$nativeTestRoot/dist/'; - - if (!_builtVersionIsExpected(cmakeBuildDir, buildOutputDir)) { - Directory(cmakeConfDir).createSync(recursive: true); - Directory(buildOutputDir).createSync(recursive: true); - File('$cmakeConfDir/main.c').writeAsStringSync(''' -int main(int argc, char *argv[]) { return 0; } -'''); - File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' -cmake_minimum_required(VERSION 3.14) -project(sentry-native-flutter-test) -add_subdirectory(../../../${platform.instance.operatingSystem} plugin) -add_executable(\${CMAKE_PROJECT_NAME} main.c) -target_link_libraries(\${CMAKE_PROJECT_NAME} PRIVATE sentry_flutter_plugin) - -# Same as generated_plugins.cmake -list(APPEND PLUGIN_BUNDLED_LIBRARIES \$) -list(APPEND PLUGIN_BUNDLED_LIBRARIES \${sentry_flutter_bundled_libraries}) -install(FILES "\${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${buildOutputDir.replaceAll('\\', '/')}" COMPONENT Runtime) -'''); - await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); - await _exec('cmake', - ['--build', cmakeBuildDir, '--config', 'Release', '--parallel']); - await _exec('cmake', ['--install', cmakeBuildDir, '--config', 'Release']); - } - return buildOutputDir; -} - -bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { - final buildCmake = File( - '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); - if (!buildCmake.existsSync()) return false; - - if (!buildCmake - .readAsStringSync() - .contains('set(PACKAGE_VERSION "$_configuredSentryNativeVersion")')) { - return false; - } - return !expectedDistFiles - .any((name) => !File('$buildOutputDir/$name').existsSync()); -} +// ignore: unused_import +import 'sentry_native_test_web.dart' + if (dart.library.io) 'sentry_native_test_ffi.dart' as actual; -late final _configuredSentryNativeVersion = - File('$repoRootDir/sentry-native/CMakeCache.txt') - .readAsLinesSync() - .map((line) => line.startsWith('version=') ? line.substring(8) : null) - .firstWhere((line) => line != null)!; +// Defining main() here allows us to manually run/debug from VSCode. +// If we didn't need that, we could just `export` above. +void main() => actual.main(); diff --git a/flutter/test/sentry_native/sentry_native_test_ffi.dart b/flutter/test/sentry_native/sentry_native_test_ffi.dart new file mode 100644 index 0000000000..49edb5f667 --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test_ffi.dart @@ -0,0 +1,326 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:ffi/ffi.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry/src/platform/platform.dart' as platform; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/native/c/sentry_native.dart'; +import 'package:sentry_flutter/src/native/factory.dart'; + +import '../mocks.dart'; +import '../mocks.mocks.dart'; + +late final String repoRootDir; +late final List expectedDistFiles; + +void main() { + repoRootDir = Directory.current.path.endsWith('/test') + ? Directory.current.parent.path + : Directory.current.path; + + expectedDistFiles = [ + 'sentry.dll', + 'crashpad_handler.exe', + 'crashpad_wer.dll', + ]; + + setUpAll(() async { + Directory.current = + await _buildSentryNative('$repoRootDir/temp/native-test'); + SentryNative.crashpadPath = + '${Directory.current.path}/${expectedDistFiles.firstWhere((f) => f.startsWith('crashpad_handler'))}'; + }); + + late SentryNative sut; + late SentryFlutterOptions options; + + setUp(() { + options = SentryFlutterOptions(dsn: fakeDsn) + // ignore: invalid_use_of_internal_member + ..automatedTestMode = true + ..debug = true + ..fileSystem = MemoryFileSystem.test(); + sut = createBinding(options) as SentryNative; + }); + + test('expected output files', () { + for (var name in expectedDistFiles) { + if (!File(name).existsSync()) { + fail('Native distribution file $name does not exist'); + } + } + }); + + test('options', () { + options + ..debug = true + ..environment = 'foo' + ..release = 'foo@bar+1' + ..enableAutoSessionTracking = true + ..dist = 'distfoo' + ..maxBreadcrumbs = 42; + + final cOptions = sut.createOptions(options); + try { + expect( + SentryNative.native + .options_get_dsn(cOptions) + .cast() + .toDartString(), + fakeDsn); + expect( + SentryNative.native + .options_get_environment(cOptions) + .cast() + .toDartString(), + 'foo'); + expect( + SentryNative.native + .options_get_release(cOptions) + .cast() + .toDartString(), + 'foo@bar+1'); + expect( + SentryNative.native.options_get_auto_session_tracking(cOptions), 1); + expect(SentryNative.native.options_get_max_breadcrumbs(cOptions), 42); + } finally { + SentryNative.native.options_free(cOptions); + } + }); + + test('SDK version', () { + expect(_configuredSentryNativeVersion.length, greaterThanOrEqualTo(5)); + expect(SentryNative.native.sdk_version().cast().toDartString(), + _configuredSentryNativeVersion); + }); + + test('SDK name', () { + expect(SentryNative.native.sdk_name().cast().toDartString(), + 'sentry.native.flutter'); + }); + + test('init', () async { + addTearDown(sut.close); + await sut.init(MockHub()); + }); + + test('app start', () { + expect(sut.fetchNativeAppStart(), null); + }); + + test('frames tracking', () { + sut.beginNativeFrames(); + expect(sut.endNativeFrames(SentryId.newId()), null); + }); + + test('hang tracking', () { + sut.pauseAppHangTracking(); + sut.resumeAppHangTracking(); + }); + + test('setUser', () async { + final user = SentryUser( + id: "fixture-id", + username: 'username', + email: 'mail@domain.tld', + ipAddress: '1.2.3.4', + name: 'User Name', + data: { + 'str': 'foo-bar', + 'double': 1.0, + 'int': 1, + 'int64': 0x7FFFFFFF + 1, + 'boo': true, + 'inner-map': {'str': 'inner'}, + 'unsupported': Object() + }, + ); + + await sut.setUser(user); + }); + + test('addBreadcrumb', () async { + final breadcrumb = Breadcrumb( + type: 'type', + message: 'message', + category: 'category', + ); + await sut.addBreadcrumb(breadcrumb); + }); + + test('clearBreadcrumbs', () async { + await sut.clearBreadcrumbs(); + }); + + test('displayRefreshRate', () async { + expect(sut.displayRefreshRate(), isNull); + }); + + test('setContexts', () async { + final value = {'object': Object()}; + await sut.setContexts('fixture-key', value); + }); + + test('removeContexts', () async { + await sut.removeContexts('fixture-key'); + }); + + test('setExtra', () async { + final value = {'object': Object()}; + await sut.setExtra('fixture-key', value); + }); + + test('removeExtra', () async { + await sut.removeExtra('fixture-key'); + }); + + test('setTag', () async { + await sut.setTag('fixture-key', 'fixture-value'); + }); + + test('removeTag', () async { + await sut.removeTag('fixture-key'); + }); + + test('startProfiler', () { + expect(() => sut.startProfiler(SentryId.newId()), throwsUnsupportedError); + }); + + test('discardProfiler', () async { + expect(() => sut.discardProfiler(SentryId.newId()), throwsUnsupportedError); + }); + + test('collectProfile', () async { + final traceId = SentryId.newId(); + const startTime = 42; + const endTime = 50; + expect(() => sut.collectProfile(traceId, startTime, endTime), + throwsUnsupportedError); + }); + + test('captureEnvelope', () async { + final data = Uint8List.fromList([1, 2, 3]); + expect(() => sut.captureEnvelope(data, false), throwsUnsupportedError); + }); + + test('loadContexts', () async { + expect(await sut.loadContexts(), isNull); + }); + + test('loadDebugImages', () async { + final list = await sut.loadDebugImages(SentryStackTrace(frames: [])); + expect(list, isNotEmpty); + expect(list![0].type, 'pe'); + expect(list[0].debugId!.length, greaterThan(30)); + expect(list[0].debugFile, isNotEmpty); + expect(list[0].imageSize, greaterThan(0)); + expect(list[0].imageAddr, startsWith('0x')); + expect(list[0].imageAddr?.length, greaterThan(2)); + expect(list[0].codeId!.length, greaterThan(10)); + expect(list[0].codeFile, isNotEmpty); + expect( + File(list[0].codeFile!), + (File file) => file.existsSync(), + ); + }); + + test('getAppDebugImage returns app.so debug image', () async { + await options.fileSystem.directory('/path/to/data').create(recursive: true); + await options.fileSystem + .file('/path/to/data/app.so') + .writeAsString('12345'); + + final image = await sut.getAppDebugImage( + SentryStackTrace( + frames: [], + // ignore: invalid_use_of_internal_member + buildId: '4c6950bd9e9cc9839071742a7295c09e', + // ignore: invalid_use_of_internal_member + baseAddr: '0x123', + ), + [DebugImage(type: 'pe', codeFile: '/path/to/application.exe')], + ); + + expect(image, isNotNull); + expect(image!.codeFile, '/path/to/data/app.so'); + expect(image.codeId, '4c6950bd9e9cc9839071742a7295c09e'); + expect(image.debugId, 'bd50694c9c9e83c99071742a7295c09e'); + expect(image.imageAddr, '0x123'); + expect(image.imageSize, 5); + }); +} + +/// Runs [command] with command's stdout and stderr being forwrarded to +/// test runner's respective streams. It buffers stdout and returns it. +/// +/// Returns [_CommandResult] with exitCode and stdout as a single sting +Future _exec(String executable, List arguments) async { + final process = await Process.start(executable, arguments); + + // forward standard streams + unawaited(stderr.addStream(process.stderr)); + unawaited(stdout.addStream(process.stdout)); + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "$executable ${arguments.join(' ')} failed with exit code $exitCode"); + } +} + +/// Compile sentry-native using CMake, as if it was part of a Flutter app. +/// Returns the directory containing built libraries +Future _buildSentryNative(String nativeTestRoot) async { + final cmakeBuildDir = '$nativeTestRoot/build'; + final cmakeConfDir = '$nativeTestRoot/conf'; + final buildOutputDir = '$nativeTestRoot/dist/'; + + if (!_builtVersionIsExpected(cmakeBuildDir, buildOutputDir)) { + Directory(cmakeConfDir).createSync(recursive: true); + Directory(buildOutputDir).createSync(recursive: true); + File('$cmakeConfDir/main.c').writeAsStringSync(''' +int main(int argc, char *argv[]) { return 0; } +'''); + File('$cmakeConfDir/CMakeLists.txt').writeAsStringSync(''' +cmake_minimum_required(VERSION 3.14) +project(sentry-native-flutter-test) +add_subdirectory(../../../${platform.instance.operatingSystem} plugin) +add_executable(\${CMAKE_PROJECT_NAME} main.c) +target_link_libraries(\${CMAKE_PROJECT_NAME} PRIVATE sentry_flutter_plugin) + +# Same as generated_plugins.cmake +list(APPEND PLUGIN_BUNDLED_LIBRARIES \$) +list(APPEND PLUGIN_BUNDLED_LIBRARIES \${sentry_flutter_bundled_libraries}) +install(FILES "\${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${buildOutputDir.replaceAll('\\', '/')}" COMPONENT Runtime) +'''); + await _exec('cmake', ['-B', cmakeBuildDir, cmakeConfDir]); + await _exec('cmake', + ['--build', cmakeBuildDir, '--config', 'Release', '--parallel']); + await _exec('cmake', ['--install', cmakeBuildDir, '--config', 'Release']); + } + return buildOutputDir; +} + +bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { + final buildCmake = File( + '$cmakeBuildDir/_deps/sentry-native-build/sentry-config-version.cmake'); + if (!buildCmake.existsSync()) return false; + + if (!buildCmake + .readAsStringSync() + .contains('set(PACKAGE_VERSION "$_configuredSentryNativeVersion")')) { + return false; + } + + return !expectedDistFiles + .any((name) => !File('$buildOutputDir/$name').existsSync()); +} + +late final _configuredSentryNativeVersion = + File('$repoRootDir/sentry-native/CMakeCache.txt') + .readAsLinesSync() + .map((line) => line.startsWith('version=') ? line.substring(8) : null) + .firstWhere((line) => line != null)!; diff --git a/flutter/test/sentry_native/sentry_native_test_web.dart b/flutter/test/sentry_native/sentry_native_test_web.dart new file mode 100644 index 0000000000..ab73b3a234 --- /dev/null +++ b/flutter/test/sentry_native/sentry_native_test_web.dart @@ -0,0 +1 @@ +void main() {} From 2de3fc6599447417fdcfcd1aa23214fb6445fe73 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 20:40:33 +0200 Subject: [PATCH 40/55] fixup code coverage test --- .github/workflows/flutter.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 9e76c84ec2..06d77287bb 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -103,7 +103,7 @@ jobs: with: name: sentry_flutter file: ./flutter/coverage/lcov.info - functionalities: "search" # remove after https://github.com/codecov/codecov-action/issues/600 + functionalities: 'search' # remove after https://github.com/codecov/codecov-action/issues/600 token: ${{ secrets.CODECOV_TOKEN }} - uses: VeryGoodOpenSource/very_good_coverage@c953fca3e24a915e111cc6f55f03f756dcb3964c # pin@v3.0.0 @@ -111,7 +111,8 @@ jobs: with: path: './flutter/coverage/lcov.info' min_coverage: 90 - exclude: 'lib/src/native/cocoa/binding.dart' + # 'native/c' for now because we run coverage on Linux where these are not tested yet. + exclude: 'lib/src/native/cocoa/binding.dart lib/src/native/c/*' - name: Build ${{ matrix.target }} working-directory: flutter/example From bd69052f11aed410fd72f9de149ee52135daa728 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 20:58:47 +0200 Subject: [PATCH 41/55] chore: update stacktrace tests --- dart/test/stack_trace_test.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/dart/test/stack_trace_test.dart b/dart/test/stack_trace_test.dart index 5b65359be4..3b250ddc81 100644 --- a/dart/test/stack_trace_test.dart +++ b/dart/test/stack_trace_test.dart @@ -138,6 +138,19 @@ void main() { ]); }); + test('obsoleted getStackFrames works as expected', () { + final sut = Fixture().getSut(considerInAppFramesByDefault: true); + final trace = ''' +#0 baz (file:///pathto/test.dart:50:3) +#1 bar (file:///pathto/test.dart:46:9) + '''; + final frames1 = sut.parse(trace).frames.map((frame) => frame.toJson()); + // ignore: deprecated_member_use_from_same_package + final frames2 = sut.getStackFrames(trace).map((frame) => frame.toJson()); + + expect(frames1, equals(frames2)); + }); + test('encodes an asynchronous stack trace', () { final frames = Fixture().getSut(considerInAppFramesByDefault: true).parse(''' From 0c92e29452998907e738294e073fc3b267323c61 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 20:59:57 +0200 Subject: [PATCH 42/55] fix missing deps --- flutter/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c1372f8385..93b2193226 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: meta: ^1.3.0 ffi: ^2.0.0 file: '>=6.1.4' + collection: ^1.16.0 dev_dependencies: build_runner: ^2.4.2 @@ -36,7 +37,6 @@ dev_dependencies: mockito: ^5.1.0 yaml: ^3.1.0 # needed for version match (code and pubspec) flutter_lints: ^4.0.0 - collection: ^1.16.0 remove_from_coverage: ^2.0.0 flutter_localizations: sdk: flutter From b0aa3abaa4d7bef255d76ff45fab52c95d5aefa5 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:06:30 +0200 Subject: [PATCH 43/55] formatting --- flutter/test/profiling_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/profiling_test.dart b/flutter/test/profiling_test.dart index d7a061f760..a2a0bbb861 100644 --- a/flutter/test/profiling_test.dart +++ b/flutter/test/profiling_test.dart @@ -69,7 +69,7 @@ void main() { test('dispose() calls native discard() exactly once', () async { when(mock.discardProfiler(any)).thenReturn(() {}); - + sut.dispose(); sut.dispose(); // Additional calls must not have an effect. From fd2d073929771d411d7fb07bc0053692e1b9a08e Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:07:29 +0200 Subject: [PATCH 44/55] add missing override annotations --- flutter/lib/src/native/c/sentry_native.dart | 28 +++++++++++++++++++ .../lib/src/native/sentry_native_channel.dart | 2 ++ 2 files changed, 30 insertions(+) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index b531570add..fd46b64313 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -17,6 +17,7 @@ import 'utils.dart'; @internal class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { DebugImage? _appDebugImage; + @override final SentryFlutterOptions options; @visibleForTesting @@ -31,6 +32,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { void _logNotSupported(String operation) => options.logger( SentryLevel.debug, 'SentryNative: $operation is not supported'); + @override FutureOr init(Hub hub) { if (!options.enableNativeCrashHandling) { options.logger( @@ -77,23 +79,30 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { } } + @override FutureOr close() { tryCatchSync('close', native.close); } + @override FutureOr fetchNativeAppStart() => null; + @override bool get supportsCaptureEnvelope => false; + @override FutureOr captureEnvelope( Uint8List envelopeData, bool containsUnhandledException) { throw UnsupportedError('$SentryNative.captureEnvelope() is not suppurted'); } + @override FutureOr beginNativeFrames() {} + @override FutureOr endNativeFrames(SentryId id) => null; + @override FutureOr setUser(SentryUser? user) { if (user == null) { tryCatchSync('remove_user', native.remove_user); @@ -105,6 +114,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { } } + @override FutureOr addBreadcrumb(Breadcrumb breadcrumb) { tryCatchSync('add_breadcrumb', () { var cBreadcrumb = breadcrumb.toJson().toNativeValue(options.logger); @@ -112,17 +122,21 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr clearBreadcrumbs() { _logNotSupported('clearing breadcrumbs'); } + @override bool get supportsLoadContexts => false; + @override FutureOr?> loadContexts() { _logNotSupported('loading contexts'); return null; } + @override FutureOr setContexts(String key, dynamic value) { tryCatchSync('set_context', () { final cValue = dynamicToNativeValue(value, options.logger); @@ -137,6 +151,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr removeContexts(String key) { tryCatchSync('remove_context', () { final cKey = key.toNativeUtf8(); @@ -145,6 +160,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr setExtra(String key, dynamic value) { tryCatchSync('set_extra', () { final cValue = dynamicToNativeValue(value, options.logger); @@ -159,6 +175,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr removeExtra(String key) { tryCatchSync('remove_extra', () { final cKey = key.toNativeUtf8(); @@ -167,6 +184,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr setTag(String key, String value) { tryCatchSync('set_tag', () { final c = FreeableFactory(); @@ -175,6 +193,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override FutureOr removeTag(String key) { tryCatchSync('remove_tag', () { final cKey = key.toNativeUtf8(); @@ -183,21 +202,26 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { }); } + @override int? startProfiler(SentryId traceId) => throw UnsupportedError("Not supported on this platform"); + @override FutureOr discardProfiler(SentryId traceId) => throw UnsupportedError("Not supported on this platform"); + @override FutureOr?> collectProfile( SentryId traceId, int startTimeNs, int endTimeNs) => throw UnsupportedError("Not supported on this platform"); + @override FutureOr displayRefreshRate() { _logNotSupported('collecting display refresh rate'); return null; } + @override FutureOr?> loadDebugImages(SentryStackTrace stackTrace) => tryCatchAsync('get_module_list', () async { final cImages = native.get_modules_list(); @@ -311,14 +335,18 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { return String.fromCharCodes(data); } + @override FutureOr pauseAppHangTracking() {} + @override FutureOr resumeAppHangTracking() {} + @override FutureOr nativeCrash() { Pointer.fromAddress(1).cast().toDartString(); } + @override FutureOr captureReplay(bool isCrash) { _logNotSupported('capturing replay'); return SentryId.empty(); diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index aa2b690f7c..eea5894a2d 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -84,6 +84,7 @@ class SentryNativeChannel return (json != null) ? NativeAppStart.fromJson(json) : null; } + @override bool get supportsCaptureEnvelope => true; @override @@ -93,6 +94,7 @@ class SentryNativeChannel 'captureEnvelope', [envelopeData, containsUnhandledException]); } + @override bool get supportsLoadContexts => true; @override From f9a3b73660ecdca26f3cf378dcb4ad63c66a790c Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:34:24 +0200 Subject: [PATCH 45/55] merge sentry-native debug image creation to dart --- .../load_dart_debug_images_integration.dart | 75 +++++++++------- ...ad_dart_debug_images_integration_test.dart | 32 +++++-- .../load_image_list_integration.dart | 22 ++++- flutter/lib/src/native/c/sentry_native.dart | 87 ------------------- flutter/pubspec.yaml | 2 +- 5 files changed, 89 insertions(+), 129 deletions(-) diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index 41b3ecba3a..ba32299a7b 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -7,13 +7,16 @@ import '../sentry.dart'; class LoadDartDebugImagesIntegration extends Integration { @override void call(Hub hub, SentryOptions options) { - options.addEventProcessor(_LoadImageIntegrationEventProcessor(options)); - options.sdk.addIntegration('loadDartImageIntegration'); + if (options.enableDartSymbolication) { + options.addEventProcessor(LoadImageIntegrationEventProcessor(options)); + options.sdk.addIntegration('loadDartImageIntegration'); + } } } -class _LoadImageIntegrationEventProcessor implements EventProcessor { - _LoadImageIntegrationEventProcessor(this._options); +@internal +class LoadImageIntegrationEventProcessor implements EventProcessor { + LoadImageIntegrationEventProcessor(this._options); final SentryOptions _options; @@ -22,40 +25,42 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, Hint hint) async { - if (_options.enableDartSymbolication) { - final stackTrace = event.stacktrace; - if (stackTrace!.frames.any((f) => f.platform == 'native')) { - try { - _debugImage ??= createDebugImage(stackTrace); - if (_debugImage != null) { - late final DebugMeta debugMeta; - if (event.debugMeta != null) { - final images = List.from(event.debugMeta!.images); - images.add(_debugImage!); - debugMeta = event.debugMeta!.copyWith(images: images); - } else { - debugMeta = DebugMeta(images: [_debugImage!]); - } - return event.copyWith(debugMeta: debugMeta); - } - } catch (e, stack) { - _options.logger( - SentryLevel.info, - "Couldn't add Dart debug image to event. " - 'The event will still be reported.', - exception: e, - stackTrace: stack, - ); - if (_options.automatedTestMode) { - rethrow; - } + final stackTrace = event.stacktrace; + if (stackTrace!.frames.any((f) => f.platform == 'native')) { + final debugImage = getAppDebugImage(stackTrace); + if (debugImage != null) { + late final DebugMeta debugMeta; + if (event.debugMeta != null) { + final images = List.from(event.debugMeta!.images); + images.add(debugImage); + debugMeta = event.debugMeta!.copyWith(images: images); + } else { + debugMeta = DebugMeta(images: [debugImage]); } + return event.copyWith(debugMeta: debugMeta); } } return event; } + DebugImage? getAppDebugImage(SentryStackTrace stackTrace) { + try { + _debugImage ??= createDebugImage(stackTrace); + } catch (e, stack) { + _options.logger( + SentryLevel.info, + "Couldn't add Dart debug image to event. The event will still be reported.", + exception: e, + stackTrace: stack, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + return _debugImage; + } + @visibleForTesting DebugImage? createDebugImage(SentryStackTrace stackTrace) { if (stackTrace.buildId == null || stackTrace.baseAddr == null) { @@ -74,10 +79,14 @@ class _LoadImageIntegrationEventProcessor implements EventProcessor { final platform = _options.platformChecker.platform; - if (platform.isAndroid) { + if (platform.isAndroid || platform.isWindows) { type = 'elf'; debugId = _convertBuildIdToDebugId(stackTrace.buildId!, platform.endian); - codeFile = 'libapp.so'; + if (platform.isAndroid) { + codeFile = 'libapp.so'; + } else if (platform.isWindows) { + codeFile = 'data/app.so'; + } } else if (platform.isIOS || platform.isMacOS) { type = 'macho'; debugId = _formatHexToUuid(stackTrace.buildId!); diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index 2ea5989393..960120046c 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -17,6 +17,7 @@ void main() { MockPlatform.iOS(), MockPlatform.macOS(), MockPlatform.android(), + MockPlatform.windows(), ]; for (final platform in platforms) { @@ -40,7 +41,7 @@ void main() { expect(fixture.options.eventProcessors.length, 1); expect( fixture.options.eventProcessors.first.runtimeType.toString(), - '_LoadImageIntegrationEventProcessor', + 'LoadImageIntegrationEventProcessor', ); }); @@ -98,10 +99,10 @@ isolate_dso_base: 20000000 #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 '''); - if (platform.isAndroid) { - expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73'); - } else { + if (platform.isIOS || platform.isMacOS) { expect(debugImage?.debugId, 'b680cb89-0f9e-3c12-a24b-172d050dec73'); + } else { + expect(debugImage?.debugId, '89cb80b6-9e0f-123c-a24b-172d050dec73'); } }); @@ -125,10 +126,31 @@ isolate_dso_base: 40000000 #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 '''); - if (platform.isAndroid) { + if (platform.isAndroid || platform.isWindows) { expect(debugImage?.type, 'elf'); } else if (platform.isIOS || platform.isMacOS) { expect(debugImage?.type, 'macho'); + } else { + fail('missing case for platform $platform'); + } + }); + + test('sets codeFile based on platform', () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 40000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + + if (platform.isAndroid) { + expect(debugImage?.codeFile, 'libapp.so'); + } else if (platform.isWindows) { + expect(debugImage?.codeFile, 'data/app.so'); + } else if (platform.isIOS || platform.isMacOS) { + expect(debugImage?.codeFile, 'App.Framework/App'); + } else { + fail('missing case for platform $platform'); } }); diff --git a/flutter/lib/src/integrations/load_image_list_integration.dart b/flutter/lib/src/integrations/load_image_list_integration.dart index 191c06faae..3643dcb83d 100644 --- a/flutter/lib/src/integrations/load_image_list_integration.dart +++ b/flutter/lib/src/integrations/load_image_list_integration.dart @@ -1,6 +1,9 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; +// ignore: implementation_imports +import 'package:sentry/src/load_dart_debug_images_integration.dart'; + import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; @@ -14,7 +17,7 @@ class LoadImageListIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { options.addEventProcessor( - _LoadImageListIntegrationEventProcessor(_native), + _LoadImageListIntegrationEventProcessor(options, _native), ); options.sdk.addIntegration('loadImageListIntegration'); @@ -22,10 +25,13 @@ class LoadImageListIntegration extends Integration { } class _LoadImageListIntegrationEventProcessor implements EventProcessor { - _LoadImageListIntegrationEventProcessor(this._native); + _LoadImageListIntegrationEventProcessor(this._options, this._native); + final SentryFlutterOptions _options; final SentryNativeBinding _native; + late final _dartProcessor = LoadImageIntegrationEventProcessor(_options); + @override Future apply(SentryEvent event, Hint hint) async { // ignore: invalid_use_of_internal_member @@ -34,7 +40,17 @@ class _LoadImageListIntegrationEventProcessor implements EventProcessor { // if the stacktrace has native frames, we load native debug images. if (stackTrace != null && stackTrace.frames.any((frame) => 'native' == frame.platform)) { - final images = await _native.loadDebugImages(stackTrace); + var images = await _native.loadDebugImages(stackTrace); + + // On windows, we need to add the ELF debug image of the AOT code. + // See https://github.com/flutter/flutter/issues/154840 + if (_options.platformChecker.platform.isWindows) { + final debugImage = _dartProcessor.getAppDebugImage(stackTrace); + if (debugImage != null) { + images ??= List.empty(); + images.add(debugImage); + } + } if (images != null) { return event.copyWith(debugMeta: DebugMeta(images: images)); } diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index fd46b64313..6534890fc1 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:ffi'; import 'dart:typed_data'; -import 'package:collection/collection.dart'; import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; @@ -16,7 +15,6 @@ import 'utils.dart'; @internal class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { - DebugImage? _appDebugImage; @override final SentryFlutterOptions options; @@ -244,97 +242,12 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { codeId: cImage.get('code_id').castPrimitive(options.logger), ); }); - - // On windows, we need to add the ELF debug image of the AOT code. - // See https://github.com/flutter/flutter/issues/154840 - if (options.platformChecker.platform.isWindows) { - _appDebugImage ??= await getAppDebugImage(stackTrace, images); - if (_appDebugImage != null) { - images.add(_appDebugImage!); - } - } - return images; } finally { native.value_decref(cImages); } }); - @visibleForTesting - Future getAppDebugImage( - SentryStackTrace stackTrace, Iterable nativeImages) async { - // ignore: invalid_use_of_internal_member - final buildId = stackTrace.buildId; - // ignore: invalid_use_of_internal_member - final imageAddr = stackTrace.baseAddr; - - if (buildId == null || imageAddr == null) { - return null; - } - - final exePath = nativeImages - .firstWhereOrNull( - (image) => image.codeFile?.toLowerCase().endsWith('.exe') ?? false) - ?.codeFile; - if (exePath == null) { - options.logger( - SentryLevel.debug, - "Couldn't add AOT ELF image for server-side symbolication because the " - "app executable is not among the debug images reported by native."); - return null; - } - - final appSoFile = options.fileSystem - .file(exePath) - .parent - .childDirectory('data') - .childFile('app.so'); - if (!await appSoFile.exists()) { - options.logger(SentryLevel.debug, - "Couldn't add AOT ELF image because ${appSoFile.path} doesn't exist."); - return null; - } - - final stat = await appSoFile.stat(); - return DebugImage( - type: 'elf', - imageAddr: imageAddr, - imageSize: stat.size, - codeFile: appSoFile.path, - codeId: buildId, - debugId: _computeDebugId(buildId), - ); - } - - /// See https://github.com/getsentry/symbolic/blob/7dc28dd04c06626489c7536cfe8c7be8f5c48804/symbolic-debuginfo/src/elf.rs#L709-L734 - /// Converts an ELF object identifier into a `DebugId`. - /// - /// The identifier data is first truncated or extended to match 16 byte size of - /// Uuids. If the data is declared in little endian, the first three Uuid fields - /// are flipped to match the big endian expected by the breakpad processor. - /// - /// The `DebugId::appendix` field is always `0` for ELF. - String? _computeDebugId(String buildId) { - // Make sure that we have exactly UUID_SIZE bytes available - const uuidSize = 16 * 2; - final data = Uint8List(uuidSize); - final len = buildId.length.clamp(0, uuidSize); - data.setAll(0, buildId.codeUnits.take(len)); - - if (Endian.host == Endian.little) { - // The file ELF file targets a little endian architecture. Convert to - // network byte order (big endian) to match the Breakpad processor's - // expectations. For big endian object files, this is not needed. - // To manipulate this as hex, we create an Uint16 view. - final data16 = Uint16List.view(data.buffer); - data16.setRange(0, 4, data16.sublist(0, 4).reversed); - data16.setRange(4, 6, data16.sublist(4, 6).reversed); - data16.setRange(6, 8, data16.sublist(6, 8).reversed); - } - - return String.fromCharCodes(data); - } - @override FutureOr pauseAppHangTracking() {} diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 93b2193226..c1372f8385 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -28,7 +28,6 @@ dependencies: meta: ^1.3.0 ffi: ^2.0.0 file: '>=6.1.4' - collection: ^1.16.0 dev_dependencies: build_runner: ^2.4.2 @@ -37,6 +36,7 @@ dev_dependencies: mockito: ^5.1.0 yaml: ^3.1.0 # needed for version match (code and pubspec) flutter_lints: ^4.0.0 + collection: ^1.16.0 remove_from_coverage: ^2.0.0 flutter_localizations: sdk: flutter From 42a868c6e3dfb6ce5fb95b5d5223ff801ab062b0 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:43:03 +0200 Subject: [PATCH 46/55] analyzer issues --- flutter/lib/src/native/c/sentry_native.dart | 7 ++--- flutter/lib/src/native/c/utils.dart | 2 +- .../native_sdk_integration_test.dart | 4 +-- flutter/test/native_scope_observer_test.dart | 18 +++++------ flutter/test/profiling_test.dart | 2 +- .../sentry_native/sentry_native_test_ffi.dart | 31 ++----------------- 6 files changed, 18 insertions(+), 46 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index 6534890fc1..da466c7f17 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -19,8 +19,7 @@ class SentryNative with SentryNativeSafeInvoker implements SentryNativeBinding { final SentryFlutterOptions options; @visibleForTesting - static late final native = - binding.SentryNative(DynamicLibrary.open('sentry.dll')); + static final native = binding.SentryNative(DynamicLibrary.open('sentry.dll')); @visibleForTesting static String? crashpadPath; @@ -338,7 +337,7 @@ binding.sentry_value_u? dynamicToNativeValue( extension on String { binding.sentry_value_u toNativeValue() { - final cValue = this.toNativeUtf8(); + final cValue = toNativeUtf8(); final result = SentryNative.native.value_new_string(cValue.cast()); malloc.free(cValue); return result; @@ -348,7 +347,7 @@ extension on String { extension on int { binding.sentry_value_u toNativeValue() { if (this > 0x7FFFFFFF) { - return this.toString().toNativeValue(); + return toString().toNativeValue(); } else { return SentryNative.native.value_new_int32(this); } diff --git a/flutter/lib/src/native/c/utils.dart b/flutter/lib/src/native/c/utils.dart index b3f3ef5e4b..34786404e5 100644 --- a/flutter/lib/src/native/c/utils.dart +++ b/flutter/lib/src/native/c/utils.dart @@ -4,7 +4,7 @@ import 'package:ffi/ffi.dart'; /// Creates and collects native pointers that need to be freed. class FreeableFactory { - List _allocated = []; + final _allocated = []; Pointer str(String? dartString) { if (dartString == null) { diff --git a/flutter/test/integrations/native_sdk_integration_test.dart b/flutter/test/integrations/native_sdk_integration_test.dart index 42008efda5..8645a80aa2 100644 --- a/flutter/test/integrations/native_sdk_integration_test.dart +++ b/flutter/test/integrations/native_sdk_integration_test.dart @@ -15,8 +15,8 @@ void main() { setUp(() { fixture = IntegrationTestFixture(NativeSdkIntegration.new); - when(fixture.binding.init(any)).thenReturn(() {}); - when(fixture.binding.close()).thenReturn(() {}); + when(fixture.binding.init(any)).thenReturn(null); + when(fixture.binding.close()).thenReturn(null); }); test('adds integration', () async { diff --git a/flutter/test/native_scope_observer_test.dart b/flutter/test/native_scope_observer_test.dart index ea0cfbf425..e22136bc1b 100644 --- a/flutter/test/native_scope_observer_test.dart +++ b/flutter/test/native_scope_observer_test.dart @@ -18,7 +18,7 @@ void main() { }); test('addBreadcrumbCalls', () async { - when(mock.addBreadcrumb(any)).thenReturn(() {}); + when(mock.addBreadcrumb(any)).thenReturn(null); final breadcrumb = Breadcrumb(); await sut.addBreadcrumb(breadcrumb); @@ -26,14 +26,14 @@ void main() { }); test('clearBreadcrumbsCalls', () async { - when(mock.clearBreadcrumbs()).thenReturn(() {}); + when(mock.clearBreadcrumbs()).thenReturn(null); await sut.clearBreadcrumbs(); verify(mock.clearBreadcrumbs()).called(1); }); test('removeContextsCalls', () async { - when(mock.removeContexts(any)).thenReturn(() {}); + when(mock.removeContexts(any)).thenReturn(null); await sut.removeContexts('fixture-key'); expect( @@ -41,42 +41,42 @@ void main() { }); test('removeExtraCalls', () async { - when(mock.removeExtra(any)).thenReturn(() {}); + when(mock.removeExtra(any)).thenReturn(null); await sut.removeExtra('fixture-key'); expect(verify(mock.removeExtra(captureAny)).captured.single, 'fixture-key'); }); test('removeTagCalls', () async { - when(mock.removeTag(any)).thenReturn(() {}); + when(mock.removeTag(any)).thenReturn(null); await sut.removeTag('fixture-key'); expect(verify(mock.removeTag(captureAny)).captured.single, 'fixture-key'); }); test('setContextsCalls', () async { - when(mock.setContexts(any, any)).thenReturn(() {}); + when(mock.setContexts(any, any)).thenReturn(null); await sut.setContexts('fixture-key', 'fixture-value'); verify(mock.setContexts('fixture-key', 'fixture-value')).called(1); }); test('setExtraCalls', () async { - when(mock.setExtra(any, any)).thenReturn(() {}); + when(mock.setExtra(any, any)).thenReturn(null); await sut.setExtra('fixture-key', 'fixture-value'); verify(mock.setExtra('fixture-key', 'fixture-value')).called(1); }); test('setTagCalls', () async { - when(mock.setTag(any, any)).thenReturn(() {}); + when(mock.setTag(any, any)).thenReturn(null); await sut.setTag('fixture-key', 'fixture-value'); verify(mock.setTag('fixture-key', 'fixture-value')).called(1); }); test('setUserCalls', () async { - when(mock.setUser(any)).thenReturn(() {}); + when(mock.setUser(any)).thenReturn(null); final user = SentryUser(id: 'foo bar'); await sut.setUser(user); diff --git a/flutter/test/profiling_test.dart b/flutter/test/profiling_test.dart index a2a0bbb861..510daac9f0 100644 --- a/flutter/test/profiling_test.dart +++ b/flutter/test/profiling_test.dart @@ -68,7 +68,7 @@ void main() { }); test('dispose() calls native discard() exactly once', () async { - when(mock.discardProfiler(any)).thenReturn(() {}); + when(mock.discardProfiler(any)).thenReturn(null); sut.dispose(); sut.dispose(); // Additional calls must not have an effect. diff --git a/flutter/test/sentry_native/sentry_native_test_ffi.dart b/flutter/test/sentry_native/sentry_native_test_ffi.dart index 49edb5f667..988e32181c 100644 --- a/flutter/test/sentry_native/sentry_native_test_ffi.dart +++ b/flutter/test/sentry_native/sentry_native_test_ffi.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; -import 'package:file/memory.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry/src/platform/platform.dart' as platform; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -41,8 +40,7 @@ void main() { options = SentryFlutterOptions(dsn: fakeDsn) // ignore: invalid_use_of_internal_member ..automatedTestMode = true - ..debug = true - ..fileSystem = MemoryFileSystem.test(); + ..debug = true; sut = createBinding(options) as SentryNative; }); @@ -226,31 +224,6 @@ void main() { (File file) => file.existsSync(), ); }); - - test('getAppDebugImage returns app.so debug image', () async { - await options.fileSystem.directory('/path/to/data').create(recursive: true); - await options.fileSystem - .file('/path/to/data/app.so') - .writeAsString('12345'); - - final image = await sut.getAppDebugImage( - SentryStackTrace( - frames: [], - // ignore: invalid_use_of_internal_member - buildId: '4c6950bd9e9cc9839071742a7295c09e', - // ignore: invalid_use_of_internal_member - baseAddr: '0x123', - ), - [DebugImage(type: 'pe', codeFile: '/path/to/application.exe')], - ); - - expect(image, isNotNull); - expect(image!.codeFile, '/path/to/data/app.so'); - expect(image.codeId, '4c6950bd9e9cc9839071742a7295c09e'); - expect(image.debugId, 'bd50694c9c9e83c99071742a7295c09e'); - expect(image.imageAddr, '0x123'); - expect(image.imageSize, 5); - }); } /// Runs [command] with command's stdout and stderr being forwrarded to @@ -319,7 +292,7 @@ bool _builtVersionIsExpected(String cmakeBuildDir, String buildOutputDir) { .any((name) => !File('$buildOutputDir/$name').existsSync()); } -late final _configuredSentryNativeVersion = +final _configuredSentryNativeVersion = File('$repoRootDir/sentry-native/CMakeCache.txt') .readAsLinesSync() .map((line) => line.startsWith('version=') ? line.substring(8) : null) From 575de3fb668409cbbd6aad2ee0fc9acb169edd9b Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:50:45 +0200 Subject: [PATCH 47/55] disable analysis on generated code --- flutter/analysis_options.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml index d95702842f..5763c67d69 100644 --- a/flutter/analysis_options.yaml +++ b/flutter/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: - test/*.mocks.dart + - lib/src/native/c/binding.dart language: strict-casts: true strict-inference: true From 315b576765f3f849fded9ef0fdb7795a6dc4645a Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 18 Sep 2024 21:53:24 +0200 Subject: [PATCH 48/55] chore: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7b75b7bf..14a2b881a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) + ### Enhancements - Improve app start integration ([#2266](https://github.com/getsentry/sentry-dart/pull/2266)) From d63e65415500e3d33edd48ef92c14945b5e6d3f3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 19 Sep 2024 20:15:19 +0200 Subject: [PATCH 49/55] analyzer issues --- .github/workflows/flutter.yml | 2 +- flutter/analysis_options.yaml | 1 - flutter/ffi-native.yaml | 2 ++ flutter/lib/src/native/c/binding.dart | 2 ++ 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/flutter.yml b/.github/workflows/flutter.yml index 06d77287bb..e43f76a618 100644 --- a/.github/workflows/flutter.yml +++ b/.github/workflows/flutter.yml @@ -111,7 +111,7 @@ jobs: with: path: './flutter/coverage/lcov.info' min_coverage: 90 - # 'native/c' for now because we run coverage on Linux where these are not tested yet. + # 'native/c' for now because we run coverage on Linux where these are not tested yet. exclude: 'lib/src/native/cocoa/binding.dart lib/src/native/c/*' - name: Build ${{ matrix.target }} diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml index 5763c67d69..d95702842f 100644 --- a/flutter/analysis_options.yaml +++ b/flutter/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: - test/*.mocks.dart - - lib/src/native/c/binding.dart language: strict-casts: true strict-inference: true diff --git a/flutter/ffi-native.yaml b/flutter/ffi-native.yaml index 4d59bfaa26..cdb28b223b 100644 --- a/flutter/ffi-native.yaml +++ b/flutter/ffi-native.yaml @@ -71,3 +71,5 @@ unions: comments: style: any length: full +preamble: | + // ignore_for_file: unused_field diff --git a/flutter/lib/src/native/c/binding.dart b/flutter/lib/src/native/c/binding.dart index 059d9c9eac..5d944edf0b 100644 --- a/flutter/lib/src/native/c/binding.dart +++ b/flutter/lib/src/native/c/binding.dart @@ -1,3 +1,5 @@ +// ignore_for_file: unused_field + // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. From 7e139297e1e58a394d24ad71e331786a80a3157f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 19 Sep 2024 20:58:50 +0200 Subject: [PATCH 50/55] fix tests --- .../load_dart_debug_images_integration.dart | 8 +++++++- ...ad_dart_debug_images_integration_test.dart | 20 +++++++++++++++++++ .../integrations/load_image_list_test.dart | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/dart/lib/src/load_dart_debug_images_integration.dart b/dart/lib/src/load_dart_debug_images_integration.dart index ba32299a7b..4b7a35c39f 100644 --- a/dart/lib/src/load_dart_debug_images_integration.dart +++ b/dart/lib/src/load_dart_debug_images_integration.dart @@ -26,7 +26,7 @@ class LoadImageIntegrationEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, Hint hint) async { final stackTrace = event.stacktrace; - if (stackTrace!.frames.any((f) => f.platform == 'native')) { + if (stackTrace != null) { final debugImage = getAppDebugImage(stackTrace); if (debugImage != null) { late final DebugMeta debugMeta; @@ -45,6 +45,12 @@ class LoadImageIntegrationEventProcessor implements EventProcessor { } DebugImage? getAppDebugImage(SentryStackTrace stackTrace) { + // Don't return the debug image if the stack trace doesn't have native info. + if (stackTrace.baseAddr == null || + stackTrace.buildId == null || + !stackTrace.frames.any((f) => f.platform == 'native')) { + return null; + } try { _debugImage ??= createDebugImage(stackTrace); } catch (e, stack) { diff --git a/dart/test/load_dart_debug_images_integration_test.dart b/dart/test/load_dart_debug_images_integration_test.dart index 960120046c..84d478ab08 100644 --- a/dart/test/load_dart_debug_images_integration_test.dart +++ b/dart/test/load_dart_debug_images_integration_test.dart @@ -84,6 +84,26 @@ isolate_dso_base: 10000000 expect(debugImage?.imageAddr, equals('0x10000000')); }); + test( + 'Event processor does not add debug image on second stack trace without image address', + () async { + final debugImage = await fixture.parseAndProcess(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** +build_id: 'b680cb890f9e3c12a24b172d050dec73' +isolate_dso_base: 10000000 + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +'''); + expect(debugImage?.debugId, isNotEmpty); + expect(debugImage?.imageAddr, equals('0x10000000')); + + final event = fixture.newEvent(stackTrace: fixture.parse(''' +*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7 +''')); + final resultEvent = await fixture.process(event); + expect(resultEvent?.debugMeta?.images, isEmpty); + }); + test('returns null for invalid stack trace', () async { final event = fixture.newEvent(stackTrace: fixture.parse('Invalid stack trace')); diff --git a/flutter/test/integrations/load_image_list_test.dart b/flutter/test/integrations/load_image_list_test.dart index 1788d187b1..95e31a625c 100644 --- a/flutter/test/integrations/load_image_list_test.dart +++ b/flutter/test/integrations/load_image_list_test.dart @@ -27,7 +27,7 @@ void main() { setUp(() async { fixture = IntegrationTestFixture(LoadImageListIntegration.new); when(fixture.binding.loadDebugImages(any)) - .thenAnswer((_) async => imageList); + .thenAnswer((_) async => imageList.toList()); await fixture.registerIntegration(); }); From 47017917eed72a2bc33f5c7fe7d757cdca771ff1 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Fri, 20 Sep 2024 12:23:51 +0200 Subject: [PATCH 51/55] Revert "tmp: use sentry.init in the flutter example" This reverts commit 1c7b63d535db6a4221f6c8f7211fb1f45cc41b6b. --- flutter/example/lib/main.dart | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 1967827f02..f34a1f5a7f 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -62,36 +62,36 @@ Future setupSentry( bool isIntegrationTest = false, BeforeSendCallback? beforeSendCallback, }) async { - await Sentry.init( + await SentryFlutter.init( (options) { options.dsn = exampleDsn; options.tracesSampleRate = 1.0; - // options.profilesSampleRate = 1.0; - // options.reportPackages = false; + options.profilesSampleRate = 1.0; + options.reportPackages = false; options.addInAppInclude('sentry_flutter_example'); options.considerInAppFramesByDefault = false; options.attachThreads = true; - // options.enableWindowMetricBreadcrumbs = true; + options.enableWindowMetricBreadcrumbs = true; options.addIntegration(LoggingIntegration(minEventLevel: Level.INFO)); options.sendDefaultPii = true; - // options.reportSilentFlutterErrors = true; - // options.attachScreenshot = true; - // options.screenshotQuality = SentryScreenshotQuality.low; - // options.attachViewHierarchy = true; + options.reportSilentFlutterErrors = true; + options.attachScreenshot = true; + options.screenshotQuality = SentryScreenshotQuality.low; + options.attachViewHierarchy = true; // We can enable Sentry debug logging during development. This is likely // going to log too much for your app, but can be useful when figuring out // configuration issues, e.g. finding out why your events are not uploaded. options.debug = true; options.spotlight = Spotlight(enabled: true); - // options.enableTimeToFullDisplayTracing = true; + options.enableTimeToFullDisplayTracing = true; options.enableMetrics = true; options.maxRequestBodySize = MaxRequestBodySize.always; options.maxResponseBodySize = MaxResponseBodySize.always; - // options.navigatorKey = navigatorKey; + options.navigatorKey = navigatorKey; - // options.experimental.replay.sessionSampleRate = 1.0; - // options.experimental.replay.onErrorSampleRate = 1.0; + options.experimental.replay.sessionSampleRate = 1.0; + options.experimental.replay.onErrorSampleRate = 1.0; _isIntegrationTest = isIntegrationTest; if (_isIntegrationTest) { From 5fa09e24d7d20e411244fdb77cc4989ebf755bd7 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 25 Sep 2024 10:52:21 +0200 Subject: [PATCH 52/55] test: fix flutter test --- flutter/test/sentry_flutter_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index d48a428b6b..233f4539bf 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -59,6 +59,7 @@ void main() { setUp(() async { native = NativeChannelFixture(); + SentryFlutter.native = null; }); group('Test platform integrations', () { From 72547cfaf5ae26f44b5f5ad96c181d90706f2baa Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Wed, 9 Oct 2024 19:35:51 +0200 Subject: [PATCH 53/55] fix int.toNativeValue() range --- flutter/lib/src/native/c/sentry_native.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/native/c/sentry_native.dart b/flutter/lib/src/native/c/sentry_native.dart index da466c7f17..d3f86df2d7 100644 --- a/flutter/lib/src/native/c/sentry_native.dart +++ b/flutter/lib/src/native/c/sentry_native.dart @@ -346,10 +346,10 @@ extension on String { extension on int { binding.sentry_value_u toNativeValue() { - if (this > 0x7FFFFFFF) { - return toString().toNativeValue(); - } else { + if (this >= -2147483648 && this <= 2147483647) { return SentryNative.native.value_new_int32(this); + } else { + return toString().toNativeValue(); } } } From 3383f9c1811655ba3cf1de5c83386fdd86e134a3 Mon Sep 17 00:00:00 2001 From: Ivan Dlugos Date: Thu, 10 Oct 2024 09:23:56 +0200 Subject: [PATCH 54/55] comments --- flutter/test/sentry_native/sentry_native_test_ffi.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/test/sentry_native/sentry_native_test_ffi.dart b/flutter/test/sentry_native/sentry_native_test_ffi.dart index 988e32181c..75fbf89c96 100644 --- a/flutter/test/sentry_native/sentry_native_test_ffi.dart +++ b/flutter/test/sentry_native/sentry_native_test_ffi.dart @@ -15,6 +15,8 @@ import '../mocks.mocks.dart'; late final String repoRootDir; late final List expectedDistFiles; +// NOTE: Don't run/debug this main(), it likely won't work. +// You can use main() in `sentry_native_test.dart`. void main() { repoRootDir = Directory.current.path.endsWith('/test') ? Directory.current.parent.path From 1b6bc60b05792481c3ac113b04ac83f56bc85d4f Mon Sep 17 00:00:00 2001 From: Ivan Dlugos <6349682+vaind@users.noreply.github.com> Date: Thu, 10 Oct 2024 13:01:50 +0200 Subject: [PATCH 55/55] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e343de228a..a769358c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Features - Emit `transaction.data` inside `contexts.trace.data` ([#2284](https://github.com/getsentry/sentry-dart/pull/2284)) -- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291) +- Blocking app starts if "appLaunchedInForeground" is false. (Android only) ([#2291](https://github.com/getsentry/sentry-dart/pull/2291)) - Windows native error & obfuscation support ([#2286](https://github.com/getsentry/sentry-dart/pull/2286)) ### Enhancements