├── android_lib ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── io │ │ └── sentry │ │ └── godotplugin │ │ └── UtilityFunctions.kt └── build.gradle.kts ├── project ├── main.gd.uid ├── mobile.gd.uid ├── cli │ ├── cli_parser.gd.uid │ ├── cli_commands.gd.uid │ ├── android_cli_adapter.gd.uid │ └── android_cli_adapter.gd ├── views │ ├── tools.gd.uid │ ├── capture_events.gd.uid │ ├── demo_output.gd.uid │ ├── enrich_events.gd.uid │ ├── tools.tscn │ ├── enrich_events.gd │ ├── output_pane.tscn │ └── tools.gd ├── project_main_loop.gd.uid ├── script_with_errors.gd.uid ├── test │ ├── suites │ │ ├── test_sdk.gd.uid │ │ ├── test_event.gd.uid │ │ ├── test_feedback.gd.uid │ │ ├── test_options.gd.uid │ │ ├── test_attachment.gd.uid │ │ ├── test_breadcrumb.gd.uid │ │ ├── test_event_json.gd.uid │ │ ├── test_sentry_user.gd.uid │ │ ├── test_timestamp.gd.uid │ │ ├── test_user_json.gd.uid │ │ ├── test_breadcrumbs_json.gd.uid │ │ ├── test_contexts_json.gd.uid │ │ ├── test_event_integrity.gd.uid │ │ ├── test_logger_integration.gd.uid │ │ ├── test_feedback.gd │ │ ├── test_attachment.gd │ │ ├── test_sentry_user.gd │ │ ├── test_sdk.gd │ │ ├── test_event_integrity.gd │ │ └── test_breadcrumb.gd │ ├── util │ │ ├── test_runner.gd.uid │ │ ├── sentry_test_suite.gd.uid │ │ └── sentry_test_suite.gd │ └── isolated │ │ ├── test_logger_disabled.gd.uid │ │ ├── test_pii_disabled.gd.uid │ │ ├── test_pii_enabled.gd.uid │ │ ├── test_sdk_lifecycle.gd.uid │ │ ├── test_structured_logs.gd.uid │ │ ├── test_limit_throttling.gd.uid │ │ ├── test_logger_breadcrumbs.gd.uid │ │ ├── test_logger_with_masks.gd.uid │ │ ├── test_options_integrity.gd.uid │ │ ├── test_limit_events_per_frame.gd.uid │ │ ├── test_limit_throttling_disabled.gd.uid │ │ ├── test_logger_with_masks_empty.gd.uid │ │ ├── test_limit_repeating_error_window.gd.uid │ │ ├── test_limit_throttling_on_startup.gd.uid │ │ ├── test_pii_disabled.gd │ │ ├── test_pii_enabled.gd │ │ ├── test_options_integrity.gd │ │ ├── test_logger_disabled.gd │ │ ├── test_logger_with_masks_empty.gd │ │ ├── test_sdk_lifecycle.gd │ │ ├── test_limit_throttling_disabled.gd │ │ ├── test_limit_events_per_frame.gd │ │ ├── test_logger_breadcrumbs.gd │ │ ├── test_limit_throttling_on_startup.gd │ │ ├── test_logger_with_masks.gd │ │ ├── test_limit_repeating_error_window.gd │ │ └── test_limit_throttling.gd ├── addons │ └── sentry │ │ └── user_feedback │ │ ├── user_feedback_form.gd.uid │ │ ├── user_feedback_gui.gd.uid │ │ ├── logo.svg.import │ │ ├── logo.svg │ │ └── user_feedback_gui.tscn ├── script_with_errors.gd ├── mobile.gd ├── main.tscn ├── main.gd ├── icon.svg ├── icon.svg.import ├── desktop.tscn ├── project.godot ├── project_main_loop.gd └── mobile.tscn ├── .gitattributes ├── modules └── sentry-cocoa.properties ├── .github ├── issue_example.png ├── ISSUE_TEMPLATE │ ├── feature.md │ └── bug.yml └── workflows │ ├── danger.yml │ ├── release.yml │ ├── update-deps.yml │ ├── static_checks.yml │ ├── unit_tests.yml │ ├── test_android.yml │ ├── ci.yml │ └── test_integration.yml ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── src ├── sentry │ ├── logging │ │ ├── state.cpp │ │ └── state.h │ ├── environment.h │ ├── android │ │ ├── android_util.h │ │ ├── android_log.h │ │ ├── android_breadcrumb.h │ │ ├── android_util.cpp │ │ ├── android_event.h │ │ ├── android_breadcrumb.cpp │ │ └── android_log.cpp │ ├── uuid.h │ ├── processing │ │ ├── process_log.h │ │ ├── sentry_event_processor.cpp │ │ ├── process_event.h │ │ ├── view_hierarchy_builder.h │ │ ├── process_log.cpp │ │ ├── screenshot_processor.h │ │ ├── sentry_event_processor.h │ │ ├── view_hierarchy_processor.h │ │ ├── view_hierarchy_processor.cpp │ │ ├── process_event.cpp │ │ └── view_hierarchy_builder.cpp │ ├── util │ │ ├── screenshot.h │ │ ├── screenshot.cpp │ │ └── simple_bind.h │ ├── common_defs.h │ ├── log_level.h │ ├── sentry_feedback.cpp │ ├── level.h │ ├── runtime_config.h │ ├── sentry_log.cpp │ ├── environment.cpp │ ├── native │ │ ├── native_log.h │ │ ├── native_breadcrumb.h │ │ ├── native_util.h │ │ ├── native_sdk.h │ │ ├── native_event.h │ │ └── native_breadcrumb.cpp │ ├── cocoa │ │ ├── cocoa_log.h │ │ ├── cocoa_breadcrumb.h │ │ ├── cocoa_includes.h │ │ ├── cocoa_sdk.h │ │ ├── cocoa_breadcrumb.mm │ │ ├── cocoa_event.h │ │ └── cocoa_util.h │ ├── contexts.h │ ├── sentry_feedback.h │ ├── runtime_config.cpp │ ├── sentry_log.h │ ├── uuid.cpp │ ├── sentry_breadcrumb.h │ ├── level.cpp │ ├── sentry_logger.h │ ├── sentry_breadcrumb.cpp │ ├── sentry_user.h │ ├── disabled │ │ ├── disabled_breadcrumb.h │ │ ├── disabled_sdk.h │ │ └── disabled_event.h │ ├── godot_error_types.h │ ├── sentry_attachment.cpp │ ├── sentry_timestamp.h │ ├── internal_sdk.h │ ├── sentry_attachment.h │ ├── sentry_event.h │ └── sentry_logger.cpp └── editor │ ├── sentry_editor_plugin.h │ ├── sentry_editor_export_plugin_android.h │ ├── sentry_editor_export_plugin_unix.h │ ├── sentry_editor_export_plugin_android.cpp │ ├── sentry_editor_plugin.cpp │ └── sentry_editor_export_plugin_unix.cpp ├── .pre-commit-config.yaml ├── .craft.yml ├── .gitmodules ├── settings.gradle.kts ├── site_scons └── site_tools │ ├── copy.py │ └── plist.py ├── scripts ├── update-doc-classes.ps1 ├── bump-version.ps1 ├── post-release.ps1 └── run-isolated-tests.ps1 ├── LICENSE.md ├── .gitignore ├── gradle.properties ├── doc_classes ├── SentryExperimental.xml ├── SentryFeedback.xml └── SentryLoggerLimits.xml ├── integration_tests └── Utils.ps1 └── README.md /android_lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /project/main.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dve43oouplade 2 | -------------------------------------------------------------------------------- /project/mobile.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cw2874mkwddfr 2 | -------------------------------------------------------------------------------- /project/cli/cli_parser.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ciswxw1fixti3 2 | -------------------------------------------------------------------------------- /project/views/tools.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5i2jwygne6je 2 | -------------------------------------------------------------------------------- /project/cli/cli_commands.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bg6afsun3ekyl 2 | -------------------------------------------------------------------------------- /project/project_main_loop.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ckyjdnckvfm8b 2 | -------------------------------------------------------------------------------- /project/script_with_errors.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bb3vjn17ukj1s 2 | -------------------------------------------------------------------------------- /project/test/suites/test_sdk.gd.uid: -------------------------------------------------------------------------------- 1 | uid://x322gobxlygq 2 | -------------------------------------------------------------------------------- /project/views/capture_events.gd.uid: -------------------------------------------------------------------------------- 1 | uid://kjyxrvgox014 2 | -------------------------------------------------------------------------------- /project/views/demo_output.gd.uid: -------------------------------------------------------------------------------- 1 | uid://7pkoni236cwi 2 | -------------------------------------------------------------------------------- /project/views/enrich_events.gd.uid: -------------------------------------------------------------------------------- 1 | uid://olue16oihup3 2 | -------------------------------------------------------------------------------- /project/cli/android_cli_adapter.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bb5p3f8q35xjy 2 | -------------------------------------------------------------------------------- /project/test/suites/test_event.gd.uid: -------------------------------------------------------------------------------- 1 | uid://lsjbhxdva3dr 2 | -------------------------------------------------------------------------------- /project/test/suites/test_feedback.gd.uid: -------------------------------------------------------------------------------- 1 | uid://m3p77wja6wk0 2 | -------------------------------------------------------------------------------- /project/test/suites/test_options.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b3e503tgy081v 2 | -------------------------------------------------------------------------------- /project/test/util/test_runner.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dfywf08xk1gra 2 | -------------------------------------------------------------------------------- /project/test/suites/test_attachment.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b10m6784fkgyb 2 | -------------------------------------------------------------------------------- /project/test/suites/test_breadcrumb.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dexm1kq44m4cv 2 | -------------------------------------------------------------------------------- /project/test/suites/test_event_json.gd.uid: -------------------------------------------------------------------------------- 1 | uid://csbade1swequ 2 | -------------------------------------------------------------------------------- /project/test/suites/test_sentry_user.gd.uid: -------------------------------------------------------------------------------- 1 | uid://rjk5ca5o5nss 2 | -------------------------------------------------------------------------------- /project/test/suites/test_timestamp.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c5pddrtw1e50t 2 | -------------------------------------------------------------------------------- /project/test/suites/test_user_json.gd.uid: -------------------------------------------------------------------------------- 1 | uid://brjgkfek465cq 2 | -------------------------------------------------------------------------------- /project/test/util/sentry_test_suite.gd.uid: -------------------------------------------------------------------------------- 1 | uid://nclyr5r4in0o 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_disabled.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ddogyyftyxwdx 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_pii_disabled.gd.uid: -------------------------------------------------------------------------------- 1 | uid://byqyx8h2ev86e 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_pii_enabled.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b6kpotfjai2qo 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_sdk_lifecycle.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bnpw7sgibliel 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_structured_logs.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bsybj3oeo0wly 2 | -------------------------------------------------------------------------------- /project/test/suites/test_breadcrumbs_json.gd.uid: -------------------------------------------------------------------------------- 1 | uid://da07ug0b6mwne 2 | -------------------------------------------------------------------------------- /project/test/suites/test_contexts_json.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dkp3aia3c41ku 2 | -------------------------------------------------------------------------------- /project/test/suites/test_event_integrity.gd.uid: -------------------------------------------------------------------------------- 1 | uid://tv2qygl5smmg 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dar4xh8cs0jat 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_breadcrumbs.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cok1r2upgv8jm 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_with_masks.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cdhkv5vtptu86 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_options_integrity.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b5us3ejbhvwrc 2 | -------------------------------------------------------------------------------- /project/test/suites/test_logger_integration.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cdd0io6kfx8n8 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_events_per_frame.gd.uid: -------------------------------------------------------------------------------- 1 | uid://d6mtknh2lbo0 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling_disabled.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dnltebooqe1ue 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_with_masks_empty.gd.uid: -------------------------------------------------------------------------------- 1 | uid://btwrdc1rag6ox 2 | -------------------------------------------------------------------------------- /project/addons/sentry/user_feedback/user_feedback_form.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dmtkflr7lmh5h 2 | -------------------------------------------------------------------------------- /project/addons/sentry/user_feedback/user_feedback_gui.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b7c0cq2oneiwv 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_repeating_error_window.gd.uid: -------------------------------------------------------------------------------- 1 | uid://xxs1go61wvxd 2 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling_on_startup.gd.uid: -------------------------------------------------------------------------------- 1 | uid://5onb3kamrarg 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /modules/sentry-cocoa.properties: -------------------------------------------------------------------------------- 1 | version=8.57.3 2 | repo=https://github.com/getsentry/sentry-cocoa 3 | -------------------------------------------------------------------------------- /.github/issue_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/sentry-godot/HEAD/.github/issue_example.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/getsentry/sentry-godot/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/sentry/logging/state.cpp: -------------------------------------------------------------------------------- 1 | #include "state.h" 2 | 3 | namespace sentry::logging { 4 | 5 | thread_local bool in_message_logging = false; 6 | 7 | } //namespace sentry::logging 8 | -------------------------------------------------------------------------------- /project/script_with_errors.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | func func_with_errors(): 4 | # This script will fail to compile because this method does not exist. 5 | method_does_not_exist() 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/mirrors-clang-format 3 | rev: f9a52e87b6cdcb01b0a62b8611d9ba9f2dad0067 # v19.1.7 4 | hooks: 5 | - id: clang-format 6 | -------------------------------------------------------------------------------- /src/sentry/environment.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace godot; 4 | 5 | namespace sentry::environment { 6 | 7 | String detect_godot_environment(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: Tell us about a problem our SDK could solve but doesn't. 4 | labels: ["Godot", "Feature"] 5 | --- 6 | 7 | What problem could Sentry solve that it doesn't? 8 | -------------------------------------------------------------------------------- /.craft.yml: -------------------------------------------------------------------------------- 1 | changelogPolicy: auto 2 | preReleaseCommand: pwsh scripts/bump-version.ps1 3 | postReleaseCommand: pwsh scripts/post-release.ps1 4 | targets: 5 | - name: github 6 | - name: registry 7 | sdks: 8 | github:getsentry/sentry-godot: 9 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.9.3" 3 | kotlin = "2.0.21" 4 | 5 | [plugins] 6 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 7 | android-library = { id = "com.android.library", version.ref = "agp" } 8 | -------------------------------------------------------------------------------- /src/sentry/android/android_util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace sentry::android { 6 | 7 | godot::Variant sanitize_variant(const godot::Variant &p_value, int p_depth = 0); 8 | 9 | } // namespace sentry::android 10 | -------------------------------------------------------------------------------- /src/sentry/uuid.h: -------------------------------------------------------------------------------- 1 | #ifndef UUID_H 2 | #define UUID_H 3 | 4 | #include 5 | 6 | using namespace godot; 7 | 8 | namespace sentry::uuid { 9 | 10 | String make_uuid(); 11 | 12 | } // namespace sentry::uuid 13 | 14 | #endif // UUID_H 15 | -------------------------------------------------------------------------------- /src/sentry/processing/process_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/sentry_log.h" 4 | 5 | namespace sentry { 6 | 7 | // Process log entries in before_send_log callback. 8 | Ref process_log(const Ref &p_log); 9 | 10 | } //namespace sentry 11 | -------------------------------------------------------------------------------- /src/sentry/util/screenshot.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENSHOT_H 2 | #define SCREENSHOT_H 3 | 4 | namespace godot { 5 | 6 | class PackedByteArray; 7 | 8 | } 9 | 10 | namespace sentry::util { 11 | 12 | godot::PackedByteArray take_screenshot(); 13 | 14 | } 15 | 16 | #endif // SCREENSHOT_H 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 16 13:54:44 CEST 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/sentry/processing/sentry_event_processor.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_event_processor.h" 2 | 3 | namespace sentry { 4 | 5 | void SentryEventProcessor::_bind_methods() { 6 | ClassDB::bind_method(D_METHOD("process_event"), &SentryEventProcessor::process_event); 7 | } 8 | 9 | } // namespace sentry 10 | -------------------------------------------------------------------------------- /src/sentry/common_defs.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_COMMON_DEFS_H 2 | #define SENTRY_COMMON_DEFS_H 3 | 4 | #define SENTRY_SCREENSHOT_FN "screenshot.jpg" 5 | #define SENTRY_VIEW_HIERARCHY_FN "view-hierarchy.json" 6 | 7 | namespace sentry { 8 | 9 | constexpr int VARIANT_CONVERSION_MAX_DEPTH = 32; 10 | 11 | }; 12 | 13 | #endif // SENTRY_COMMON_DEFS_H 14 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, edited, ready_for_review] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write # needed for comments 10 | 11 | jobs: 12 | danger: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: getsentry/github-workflows/danger@v3 16 | -------------------------------------------------------------------------------- /project/mobile.gd: -------------------------------------------------------------------------------- 1 | extends CanvasLayer 2 | 3 | 4 | func _ready() -> void: 5 | get_viewport().size = Vector2(810, 1530) # simulate mobile screen on desktop 6 | get_viewport().get_window().content_scale_factor = 3.0 7 | get_viewport().get_window().content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS 8 | get_viewport().get_window().content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND 9 | -------------------------------------------------------------------------------- /src/sentry/processing/process_event.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_PROCESS_EVENT_H 2 | #define SENTRY_PROCESS_EVENT_H 3 | 4 | #include "sentry/sentry_event.h" 5 | 6 | namespace sentry { 7 | 8 | // Processes events by adding contexts, applying configured processors, 9 | // and running `before_send` callback before sending to Sentry. 10 | Ref process_event(const Ref &p_event); 11 | 12 | } //namespace sentry 13 | 14 | #endif // SENTRY_PROCESS_EVENT_H 15 | -------------------------------------------------------------------------------- /project/addons/sentry/user_feedback/logo.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="svg" 4 | type="DPITexture" 5 | uid="uid://d0o3nt85ac67i" 6 | path="res://.godot/imported/logo.svg-25820d7157ee760c1db8b8ba461e2e2f.dpitex" 7 | 8 | [deps] 9 | 10 | source_file="res://addons/sentry/user_feedback/logo.svg" 11 | dest_files=["res://.godot/imported/logo.svg-25820d7157ee760c1db8b8ba461e2e2f.dpitex"] 12 | 13 | [params] 14 | 15 | base_scale=1.0 16 | saturation=1.0 17 | color_map={} 18 | compress=true 19 | -------------------------------------------------------------------------------- /src/sentry/processing/view_hierarchy_builder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/util/utf8_buffer.h" 4 | 5 | #include 6 | 7 | namespace sentry { 8 | 9 | class ViewHierarchyBuilder { 10 | private: 11 | // Initial estimated buffer size for JSON serialization (bytes). 12 | // This value is adjusted based on past data to minimize reallocations. 13 | size_t estimated_buffer_size = 262'144; 14 | 15 | public: 16 | sentry::util::UTF8Buffer build_json(); 17 | }; 18 | 19 | } //namespace sentry 20 | -------------------------------------------------------------------------------- /project/main.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://cqiowj0jydds1"] 2 | 3 | [ext_resource type="Script" uid="uid://dve43oouplade" path="res://main.gd" id="1_p65em"] 4 | [ext_resource type="Script" uid="uid://bg6afsun3ekyl" path="res://cli/cli_commands.gd" id="2_0xm2m"] 5 | 6 | [node name="Main" type="Node"] 7 | script = ExtResource("1_p65em") 8 | 9 | [node name="CLICommands" type="Node" parent="."] 10 | unique_name_in_owner = true 11 | script = ExtResource("2_0xm2m") 12 | metadata/_custom_type_script = "uid://bg6afsun3ekyl" 13 | -------------------------------------------------------------------------------- /src/sentry/log_level.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace sentry { 6 | 7 | enum LogLevel { 8 | LOG_LEVEL_TRACE, 9 | LOG_LEVEL_DEBUG, 10 | LOG_LEVEL_INFO, 11 | LOG_LEVEL_WARN, 12 | LOG_LEVEL_ERROR, 13 | LOG_LEVEL_FATAL 14 | }; 15 | 16 | inline godot::PropertyInfo make_log_level_enum_property(const godot::String &p_name) { 17 | return godot::PropertyInfo(godot::Variant::INT, p_name, godot::PROPERTY_HINT_ENUM, "Trace,Debug,Info,Warn,Error,Fatal"); 18 | } 19 | 20 | } //namespace sentry 21 | -------------------------------------------------------------------------------- /src/sentry/processing/process_log.cpp: -------------------------------------------------------------------------------- 1 | #include "process_log.h" 2 | 3 | #include "sentry/sentry_options.h" 4 | 5 | namespace sentry { 6 | 7 | Ref process_log(const Ref &p_log) { 8 | const Callable &before_send_log = SentryOptions::get_singleton()->get_before_send_log(); 9 | if (before_send_log.is_null()) { 10 | return p_log; 11 | } 12 | 13 | Ref processed = before_send_log.call(p_log); 14 | if (processed.is_null()) { 15 | return nullptr; 16 | } 17 | 18 | return p_log; 19 | } 20 | 21 | } //namespace sentry 22 | -------------------------------------------------------------------------------- /android_lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/sentry/logging/state.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace sentry::logging { 4 | 5 | // Whether the engine logger is currently processing a message. 6 | extern thread_local bool in_message_logging; 7 | 8 | // Sets a thread-local flag indicating that we are currently logging a message. 9 | // This prevents debug output from being logged within another log operation, 10 | // which can cause errors in Godot. 11 | struct MessageScope { 12 | MessageScope() { in_message_logging = true; } 13 | ~MessageScope() { in_message_logging = false; } 14 | }; 15 | 16 | } //namespace sentry::logging 17 | -------------------------------------------------------------------------------- /project/main.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | @onready var cli_commands: CLICommands = %CLICommands 4 | 5 | 6 | func _ready() -> void: 7 | SentrySDK.logger.info("Starting UI on %s", [OS.get_name()]) 8 | 9 | if await cli_commands.check_and_execute_cli(): 10 | # Quit if a CLI command was executed 11 | get_tree().quit(cli_commands.exit_code) 12 | elif OS.get_name() in ["Android", "iOS"]: 13 | # Continue with mobile UI 14 | get_tree().change_scene_to_file.call_deferred("res://mobile.tscn") 15 | else: 16 | # Continue with desktop UI 17 | get_tree().change_scene_to_file.call_deferred("res://desktop.tscn") 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sentry-native"] 2 | path = modules/sentry-native 3 | url = https://github.com/getsentry/sentry-native.git 4 | [submodule "modules/gdUnit4"] 5 | path = modules/gdUnit4 6 | url = https://github.com/MikeSchulze/gdUnit4.git 7 | [submodule "modules/godot-cpp"] 8 | path = modules/godot-cpp 9 | url = https://github.com/godotengine/godot-cpp 10 | [submodule "project/test/util/json_assert"] 11 | path = project/test/util/json_assert 12 | url = https://github.com/getsentry/gdunit-json-assert 13 | [submodule "modules/app-runner"] 14 | path = modules/app-runner 15 | url = https://github.com/getsentry/app-runner.git 16 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google { 4 | content { 5 | includeGroupByRegex("com\\.android.*") 6 | includeGroupByRegex("com\\.google.*") 7 | includeGroupByRegex("androidx.*") 8 | } 9 | } 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "SentryAndroidGodotPlugin" 23 | include(":android_lib") 24 | -------------------------------------------------------------------------------- /src/sentry/sentry_feedback.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_feedback.h" 2 | 3 | #include "sentry/util/simple_bind.h" 4 | 5 | namespace sentry { 6 | 7 | void SentryFeedback::_bind_methods() { 8 | BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "name"), set_name, get_name); 9 | BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "contact_email"), set_contact_email, get_contact_email); 10 | BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "message"), set_message, get_message); 11 | BIND_PROPERTY(SentryFeedback, PropertyInfo(Variant::STRING, "associated_event_id"), set_associated_event_id, get_associated_event_id); 12 | } 13 | 14 | } //namespace sentry 15 | -------------------------------------------------------------------------------- /project/test/isolated/test_pii_disabled.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Test `send_default_pii` option disabled 3 | 4 | 5 | func init_sdk() -> void: 6 | SentrySDK.init(func(options: SentryOptions) -> void: 7 | options.send_default_pii = false 8 | ) 9 | 10 | 11 | ## User interface must not contain ip_adress if PII disabled. 12 | func test_pii_disabled_and_default_user_ip() -> void: 13 | SentrySDK.capture_event(SentrySDK.create_event()) 14 | 15 | var json: String = await wait_for_captured_event_json() 16 | 17 | assert_json(json).describe("User interface must NOT contain ip_address") \ 18 | .at("/user") \ 19 | .must_not_contain("ip_address") \ 20 | .verify() 21 | -------------------------------------------------------------------------------- /site_scons/site_tools/copy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tool to create copy commands with auto-clean. 3 | """ 4 | 5 | from SCons.Script import Builder, Copy, Dir, Clean 6 | import os.path 7 | 8 | 9 | def copy_command(env, target, source): 10 | result = env.Command( 11 | target, 12 | source, 13 | Copy("$TARGET", "$SOURCE") 14 | ) 15 | # SCons doesn't clean non-empty directories -- we enforce it here. 16 | # NOTE: It's important to pass target as Dir() for this to work. 17 | Clean(result, target) 18 | return result 19 | 20 | 21 | def generate(env): 22 | env.AddMethod(copy_command, "Copy") 23 | 24 | 25 | def exists(env): 26 | return True 27 | -------------------------------------------------------------------------------- /project/addons/sentry/user_feedback/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_plugin.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_EDITOR_PLUGIN_H 2 | #define SENTRY_EDITOR_PLUGIN_H 3 | 4 | #ifdef TOOLS_ENABLED 5 | 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | class SentryEditorPlugin : public EditorPlugin { 12 | GDCLASS(SentryEditorPlugin, EditorPlugin); 13 | 14 | private: 15 | Ref android_export_plugin; 16 | Ref unix_export_plugin; 17 | 18 | protected: 19 | static void _bind_methods() {} 20 | void _notification(int p_what); 21 | 22 | public: 23 | }; 24 | 25 | #endif // TOOLS_ENABLED 26 | 27 | #endif // SENTRY_EDITOR_PLUGIN_H 28 | -------------------------------------------------------------------------------- /src/sentry/level.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_LEVEL_H 2 | #define SENTRY_LEVEL_H 3 | 4 | #include 5 | #include 6 | 7 | namespace sentry { 8 | 9 | // Represents the severity of events or breadcrumbs. 10 | // In the public API, it is exposed as SentrySDK.Level enum. 11 | // And as such, VariantCaster is defined in sentry_sdk.h. 12 | enum Level { 13 | LEVEL_DEBUG = 0, 14 | LEVEL_INFO = 1, 15 | LEVEL_WARNING = 2, 16 | LEVEL_ERROR = 3, 17 | LEVEL_FATAL = 4 18 | }; 19 | 20 | godot::CharString level_as_cstring(Level level); 21 | godot::PropertyInfo make_level_enum_property(const godot::String &p_name); 22 | Level int_to_level(int p_value); 23 | 24 | } // namespace sentry 25 | 26 | #endif // SENTRY_LEVEL_H 27 | -------------------------------------------------------------------------------- /src/sentry/processing/screenshot_processor.h: -------------------------------------------------------------------------------- 1 | #ifndef SCREENSHOT_EVENT_PROCESSOR_H 2 | #define SCREENSHOT_EVENT_PROCESSOR_H 3 | 4 | #include "sentry/processing/sentry_event_processor.h" 5 | 6 | #include 7 | 8 | namespace sentry { 9 | 10 | // Event processor for capturing in-engine screenshots. 11 | class ScreenshotProcessor : public SentryEventProcessor { 12 | GDCLASS(ScreenshotProcessor, SentryEventProcessor); 13 | 14 | private: 15 | String screenshot_path; 16 | int32_t last_screenshot_frame = 0; 17 | std::mutex mutex; 18 | 19 | protected: 20 | static void _bind_methods() {} 21 | 22 | public: 23 | virtual Ref process_event(const Ref &p_event) override; 24 | 25 | ScreenshotProcessor(); 26 | }; 27 | 28 | } // namespace sentry 29 | 30 | #endif // SCREENSHOT_EVENT_PROCESSOR_H 31 | -------------------------------------------------------------------------------- /project/test/isolated/test_pii_enabled.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Test `send_default_pii` option enabled 3 | 4 | 5 | func init_sdk() -> void: 6 | SentrySDK.init(func(options: SentryOptions) -> void: 7 | options.send_default_pii = true 8 | ) 9 | 10 | 11 | ## User interface should contain ip_address set to auto if PII enabled. 12 | func test_pii_enabled_and_default_user_ip() -> void: 13 | SentrySDK.capture_event(SentrySDK.create_event()) 14 | 15 | var json: String = await wait_for_captured_event_json() 16 | 17 | assert_json(json).describe("User interface must contain ip_address") \ 18 | .at("/user") \ 19 | .must_contain("ip_address") \ 20 | .verify() 21 | 22 | assert_json(json).describe("ip_address should be set to {{auto}}") \ 23 | .at("/user/ip_address") \ 24 | .must_be("{{auto}}") \ 25 | .verify() 26 | -------------------------------------------------------------------------------- /src/sentry/runtime_config.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNTIME_CONFIG_H 2 | #define RUNTIME_CONFIG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace godot; 9 | 10 | namespace sentry { 11 | 12 | class RuntimeConfig : public RefCounted { 13 | GDCLASS(RuntimeConfig, RefCounted) 14 | private: 15 | String conf_path; 16 | Ref conf; 17 | 18 | // Cached values. 19 | String installation_id; 20 | 21 | protected: 22 | static void _bind_methods() {} 23 | 24 | public: 25 | String get_installation_id() const { return installation_id; } 26 | void set_installation_id(const String &p_id); 27 | 28 | void load_file(const String &p_conf_path); 29 | 30 | RuntimeConfig(); 31 | }; 32 | 33 | } // namespace sentry 34 | 35 | #endif // RUNTIME_CONFIG_H 36 | -------------------------------------------------------------------------------- /project/test/suites/test_feedback.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test Feedback class. 3 | 4 | 5 | func test_feedback_properties() -> void: 6 | var feedback := SentryFeedback.new() 7 | 8 | assert_str(feedback.associated_event_id).is_empty() 9 | feedback.associated_event_id = "082ce03eface41dd94b8c6b005382d5e" 10 | assert_str(feedback.associated_event_id).is_equal("082ce03eface41dd94b8c6b005382d5e") 11 | 12 | assert_str(feedback.name).is_empty() 13 | feedback.name = "Bob" 14 | assert_str(feedback.name).is_equal("Bob") 15 | 16 | assert_str(feedback.contact_email).is_empty() 17 | feedback.contact_email = "bob@example.com" 18 | assert_str(feedback.contact_email).is_equal("bob@example.com") 19 | 20 | assert_str(feedback.message).is_empty() 21 | feedback.message = "something happened" 22 | assert_str(feedback.message).is_equal("something happened") 23 | -------------------------------------------------------------------------------- /scripts/update-doc-classes.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | # Updates the built-in class reference documentation by generating XML files for 4 | # new classes and updating existing ones. This process removes and adds members 5 | # as needed, but does not handle renaming automatically. 6 | 7 | $godot = $env:GODOT 8 | 9 | if (-not $godot) { 10 | Write-Host "GODOT environment variable is not set. Defaulting to `"godot`"." 11 | $godot = "godot" 12 | } 13 | 14 | if (-not (Get-Command $godot -ErrorAction SilentlyContinue)) { 15 | Write-Error "Godot executable not found. Please set the GODOT environment variable." -CategoryActivity "ERROR" 16 | exit 1 17 | } 18 | 19 | $startDir = Get-Location 20 | $scriptDir = Split-Path -Parent (Resolve-Path $MyInvocation.MyCommand.Path) 21 | Set-Location "$scriptDir/../project" 22 | 23 | & $godot --doctool ../ --gdextension-docs 24 | 25 | Set-Location $startDir 26 | -------------------------------------------------------------------------------- /src/sentry/processing/sentry_event_processor.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_EVENT_PROCESSOR_H 2 | #define SENTRY_EVENT_PROCESSOR_H 3 | 4 | #include "sentry/sentry_event.h" 5 | 6 | #include 7 | 8 | using namespace godot; 9 | 10 | namespace sentry { 11 | 12 | // Base class for processing Sentry events before they are sent to the server. 13 | // Implementations can modify, or discard events by returning null. 14 | class SentryEventProcessor : public RefCounted { 15 | GDCLASS(SentryEventProcessor, RefCounted); 16 | 17 | protected: 18 | static void _bind_methods(); 19 | 20 | public: 21 | // Returns the same event (potentially modified) or null to discard it. 22 | virtual Ref process_event(const Ref &p_event) { return p_event; } 23 | 24 | virtual ~SentryEventProcessor() = default; 25 | }; 26 | 27 | } // namespace sentry 28 | 29 | #endif // SENTRY_EVENT_PROCESSOR_H 30 | -------------------------------------------------------------------------------- /src/sentry/processing/view_hierarchy_processor.h: -------------------------------------------------------------------------------- 1 | #ifndef VIEW_HIERARCHY_PROCESSOR_H 2 | #define VIEW_HIERARCHY_PROCESSOR_H 3 | 4 | #include "sentry/processing/sentry_event_processor.h" 5 | #include "sentry/processing/view_hierarchy_builder.h" 6 | 7 | #include 8 | 9 | namespace sentry { 10 | 11 | // Event processor for capturing the view hierarchy (aka scene tree state). 12 | class ViewHierarchyProcessor : public SentryEventProcessor { 13 | GDCLASS(ViewHierarchyProcessor, SentryEventProcessor); 14 | 15 | private: 16 | CharString json_file_path; 17 | ViewHierarchyBuilder view_hierarchy_builder; 18 | 19 | protected: 20 | static void _bind_methods() {} 21 | 22 | public: 23 | virtual Ref process_event(const Ref &p_event) override; 24 | 25 | ViewHierarchyProcessor(); 26 | }; 27 | 28 | } // namespace sentry 29 | 30 | #endif // VIEW_HIERARCHY_PROCESSOR_H 31 | -------------------------------------------------------------------------------- /src/sentry/util/screenshot.cpp: -------------------------------------------------------------------------------- 1 | #include "screenshot.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | namespace sentry::util { 12 | 13 | PackedByteArray take_screenshot() { 14 | SceneTree *sml = Object::cast_to(Engine::get_singleton()->get_main_loop()); 15 | ERR_FAIL_NULL_V_MSG(sml, PackedByteArray(), "Sentry: Failed to capture screenshot - couldn't get scene tree."); 16 | 17 | Window *main_window = sml->get_root(); 18 | ERR_FAIL_NULL_V_MSG(main_window, PackedByteArray(), "Sentry: Failed to capture screenshot - couldn't get main window."); 19 | 20 | Ref tex = main_window->get_texture(); 21 | return tex->get_image()->save_jpg_to_buffer(); 22 | } 23 | 24 | } //namespace sentry::util 25 | -------------------------------------------------------------------------------- /src/sentry/sentry_log.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_log.h" 2 | 3 | #include "sentry/util/simple_bind.h" 4 | 5 | namespace sentry { 6 | 7 | void SentryLog::_bind_methods() { 8 | BIND_PROPERTY(SentryLog, sentry::make_log_level_enum_property("level"), set_level, get_level); 9 | BIND_PROPERTY(SentryLog, PropertyInfo(Variant::STRING, "body"), set_body, get_body); 10 | 11 | ClassDB::bind_method(D_METHOD("get_attribute", "name"), &SentryLog::get_attribute); 12 | ClassDB::bind_method(D_METHOD("set_attribute", "name", "value"), &SentryLog::set_attribute); 13 | ClassDB::bind_method(D_METHOD("add_attributes", "attributes"), &SentryLog::add_attributes); 14 | ClassDB::bind_method(D_METHOD("remove_attribute", "name"), &SentryLog::remove_attribute); 15 | 16 | BIND_ENUM_CONSTANT(LOG_LEVEL_TRACE); 17 | BIND_ENUM_CONSTANT(LOG_LEVEL_DEBUG); 18 | BIND_ENUM_CONSTANT(LOG_LEVEL_INFO); 19 | BIND_ENUM_CONSTANT(LOG_LEVEL_WARN); 20 | BIND_ENUM_CONSTANT(LOG_LEVEL_ERROR); 21 | BIND_ENUM_CONSTANT(LOG_LEVEL_FATAL); 22 | } 23 | 24 | } //namespace sentry 25 | -------------------------------------------------------------------------------- /src/sentry/environment.cpp: -------------------------------------------------------------------------------- 1 | #include "environment.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace sentry::environment { 7 | 8 | String detect_godot_environment() { 9 | ERR_FAIL_NULL_V(Engine::get_singleton(), "production"); 10 | ERR_FAIL_NULL_V(OS::get_singleton(), "production"); 11 | 12 | // We need to have either "dev" or "debug" in the name to prioritize dev environments: 13 | // https://develop.sentry.dev/application-architecture/dynamic-sampling/fidelity-and-biases/#prioritize-dev-environments 14 | if (OS::get_singleton()->has_feature("dedicated_server")) { 15 | return "dedicated_server"; 16 | } else if (Engine::get_singleton()->is_editor_hint()) { 17 | return "editor_dev"; 18 | } else if (OS::get_singleton()->has_feature("editor")) { 19 | return "editor_dev_run"; 20 | } else if (OS::get_singleton()->is_debug_build()) { 21 | return "export_debug"; 22 | } else { 23 | return "export_release"; 24 | } 25 | } 26 | 27 | } //namespace sentry::environment 28 | -------------------------------------------------------------------------------- /src/sentry/native/native_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/sentry_log.h" 4 | 5 | #include "sentry.h" 6 | 7 | namespace sentry::native { 8 | 9 | class NativeLog : public SentryLog { 10 | GDCLASS(NativeLog, SentryLog); 11 | 12 | private: 13 | sentry_value_t native_log; 14 | 15 | protected: 16 | static void _bind_methods() {} 17 | 18 | public: 19 | virtual LogLevel get_level() const override; 20 | virtual void set_level(LogLevel p_level) override; 21 | 22 | virtual String get_body() const override; 23 | virtual void set_body(const String &p_body) override; 24 | 25 | virtual Variant get_attribute(const String &p_name) const override; 26 | virtual void set_attribute(const String &p_name, const Variant &p_value) override; 27 | virtual void add_attributes(const Dictionary &p_attributes) override; 28 | virtual void remove_attribute(const String &p_name) override; 29 | 30 | NativeLog(); 31 | NativeLog(sentry_value_t p_native_log); 32 | virtual ~NativeLog() override; 33 | }; 34 | 35 | } //namespace sentry::native 36 | -------------------------------------------------------------------------------- /project/views/tools.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cywnvytpa2bec"] 2 | 3 | [ext_resource type="Script" uid="uid://5i2jwygne6je" path="res://views/tools.gd" id="1_wuhpo"] 4 | 5 | [node name="Tools" type="VBoxContainer"] 6 | script = ExtResource("1_wuhpo") 7 | metadata/_tab_index = 2 8 | 9 | [node name="Header - Actions" type="Label" parent="."] 10 | custom_minimum_size = Vector2(0, 40.505) 11 | layout_mode = 2 12 | text = "ACTIONS" 13 | horizontal_alignment = 1 14 | vertical_alignment = 2 15 | 16 | [node name="RunTestsButton" type="Button" parent="."] 17 | unique_name_in_owner = true 18 | layout_mode = 2 19 | text = "Run unit tests" 20 | 21 | [node name="TestDiverseContextButton" type="Button" parent="."] 22 | unique_name_in_owner = true 23 | layout_mode = 2 24 | text = "Test diverse context" 25 | 26 | [connection signal="pressed" from="RunTestsButton" to="." method="_on_run_tests_button_pressed"] 27 | [connection signal="pressed" from="TestDiverseContextButton" to="." method="_on_test_diverse_context_button_pressed"] 28 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/cocoa/cocoa_includes.h" 4 | #include "sentry/sentry_log.h" 5 | 6 | namespace sentry::cocoa { 7 | 8 | class CocoaLog : public sentry::SentryLog { 9 | GDCLASS(CocoaLog, SentryLog) 10 | private: 11 | objc::SentryLog *cocoa_log; 12 | 13 | protected: 14 | static void _bind_methods() {} 15 | 16 | public: 17 | virtual LogLevel get_level() const override; 18 | virtual void set_level(LogLevel p_level) override; 19 | 20 | virtual String get_body() const override; 21 | virtual void set_body(const String &p_body) override; 22 | 23 | virtual Variant get_attribute(const String &p_name) const override; 24 | virtual void set_attribute(const String &p_name, const Variant &p_value) override; 25 | virtual void add_attributes(const Dictionary &p_attributes) override; 26 | virtual void remove_attribute(const String &p_name) override; 27 | 28 | CocoaLog(); 29 | CocoaLog(objc::SentryLog *p_log); 30 | virtual ~CocoaLog() override; 31 | }; 32 | 33 | } // namespace sentry::cocoa 34 | -------------------------------------------------------------------------------- /project/test/isolated/test_options_integrity.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Verify that the options set in a configuration callback are correctly reflected in event objects. 3 | 4 | 5 | signal callback_processed 6 | 7 | 8 | func before() -> void: 9 | SentrySDK.init(func(options: SentryOptions) -> void: 10 | options.release = "1.2.3" 11 | options.environment = "testing" 12 | 13 | options.before_send = _before_send 14 | ) 15 | 16 | 17 | func _before_send(ev: SentryEvent) -> SentryEvent: 18 | if ev.is_crash(): 19 | # Likely processing previous crash. 20 | return ev 21 | assert_str(ev.release).is_equal("1.2.3") 22 | assert_str(ev.environment).is_equal("testing") 23 | callback_processed.emit() 24 | return null 25 | 26 | 27 | ## Verify that the options are correctly propagated to event objects. 28 | func test_options_integrity() -> void: 29 | var ev := SentrySDK.create_event() 30 | var monitor := monitor_signals(self, false) 31 | SentrySDK.capture_event(ev) 32 | await assert_signal(monitor).is_emitted("callback_processed") 33 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_export_plugin_android.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_EDITOR_EXPORT_PLUGIN_ANDROID_H 2 | #define SENTRY_EDITOR_EXPORT_PLUGIN_ANDROID_H 3 | 4 | #ifdef TOOLS_ENABLED 5 | 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | class SentryEditorExportPluginAndroid : public EditorExportPlugin { 12 | GDCLASS(SentryEditorExportPluginAndroid, EditorExportPlugin); 13 | 14 | protected: 15 | static void _bind_methods() {} 16 | 17 | public: 18 | virtual String _get_name() const override; 19 | virtual bool _supports_platform(const Ref &p_platform) const override; 20 | virtual PackedStringArray _get_android_libraries(const Ref &p_platform, bool p_debug) const override; 21 | virtual PackedStringArray _get_android_dependencies(const Ref &p_platform, bool p_debug) const override; 22 | }; 23 | 24 | #endif // TOOLS_ENABLED 25 | 26 | #endif // SENTRY_EDITOR_EXPORT_PLUGIN_ANDROID_H 27 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_export_plugin_unix.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_EDITOR_EXPORT_PLUGIN_UNIX_H 2 | #define SENTRY_EDITOR_EXPORT_PLUGIN_UNIX_H 3 | 4 | #if defined(TOOLS_ENABLED) && !defined(WINDOWS_ENABLED) 5 | 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | class SentryEditorExportPluginUnix : public EditorExportPlugin { 12 | GDCLASS(SentryEditorExportPluginUnix, EditorExportPlugin); 13 | 14 | private: 15 | String export_path; 16 | 17 | protected: 18 | static void _bind_methods() {} 19 | 20 | public: 21 | virtual String _get_name() const override; 22 | virtual bool _supports_platform(const Ref &p_platform) const override; 23 | virtual void _export_begin(const PackedStringArray &p_features, bool p_is_debug, const String &p_path, uint32_t p_flags) override; 24 | virtual void _export_end() override; 25 | }; 26 | 27 | #endif // # TOOLS_ENABLED && !WINDOWS_ENABLED 28 | 29 | #endif // SENTRY_EDITOR_EXPORT_PLUGIN_UNIX_H 30 | -------------------------------------------------------------------------------- /src/sentry/contexts.h: -------------------------------------------------------------------------------- 1 | #ifndef CONTEXTS_H 2 | #define CONTEXTS_H 3 | 4 | #include "runtime_config.h" 5 | 6 | #include 7 | #include 8 | 9 | using namespace godot; 10 | 11 | namespace sentry::contexts { 12 | 13 | Dictionary make_device_context(const Ref &p_runtime_config); 14 | 15 | // Returns smaller device context dictionary that only includes values that are 16 | // dynamic and need to be updated right before the event is sent. 17 | Dictionary make_device_context_update(); 18 | 19 | Dictionary make_app_context(); 20 | Dictionary make_gpu_context(); 21 | Dictionary make_culture_context(); 22 | Dictionary make_display_context(); 23 | Dictionary make_godot_engine_context(); 24 | Dictionary make_environment_context(); 25 | Dictionary make_performance_context(); 26 | 27 | // Creates contexts that can only be generated right before an event, e.g. performance info. 28 | HashMap make_event_contexts(); 29 | 30 | } //namespace sentry::contexts 31 | 32 | #endif // CONTEXTS_H 33 | -------------------------------------------------------------------------------- /src/sentry/sentry_feedback.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using namespace godot; 6 | 7 | namespace sentry { 8 | 9 | class SentryFeedback : public RefCounted { 10 | GDCLASS(SentryFeedback, RefCounted); 11 | 12 | private: 13 | String name; 14 | String contact_email; 15 | String message; 16 | String associated_event_id; 17 | 18 | protected: 19 | static void _bind_methods(); 20 | 21 | public: 22 | String get_name() const { return name; } 23 | void set_name(const String &p_name) { name = p_name; } 24 | 25 | String get_contact_email() const { return contact_email; } 26 | void set_contact_email(const String &p_contact_email) { contact_email = p_contact_email; } 27 | 28 | String get_message() const { return message; } 29 | void set_message(const String &p_message) { message = p_message; } 30 | 31 | String get_associated_event_id() const { return associated_event_id; } 32 | void set_associated_event_id(const String &p_associated_event_id) { associated_event_id = p_associated_event_id; } 33 | }; 34 | 35 | } //namespace sentry 36 | -------------------------------------------------------------------------------- /src/sentry/runtime_config.cpp: -------------------------------------------------------------------------------- 1 | #include "runtime_config.h" 2 | 3 | #include "sentry/uuid.h" 4 | 5 | namespace { 6 | 7 | inline String _ensure_string(const Variant &p_value, const String &p_fallback) { 8 | return p_value.get_type() == Variant::STRING ? (String)p_value : p_fallback; 9 | } 10 | 11 | } // unnamed namespace 12 | 13 | namespace sentry { 14 | 15 | void RuntimeConfig::set_installation_id(const String &p_id) { 16 | ERR_FAIL_COND(p_id.length() == 0); 17 | 18 | installation_id = p_id; 19 | conf->set_value("main", "installation_id", (String)installation_id); 20 | conf->save(conf_path); 21 | } 22 | 23 | void RuntimeConfig::load_file(const String &p_conf_path) { 24 | ERR_FAIL_COND(p_conf_path.is_empty()); 25 | 26 | conf_path = p_conf_path; 27 | conf->load(conf_path); 28 | 29 | installation_id = _ensure_string(conf->get_value("main", "installation_id", ""), ""); 30 | if (installation_id.is_empty()) { 31 | set_installation_id(sentry::uuid::make_uuid()); 32 | } 33 | } 34 | 35 | RuntimeConfig::RuntimeConfig() { 36 | conf.instantiate(); 37 | } 38 | 39 | } // namespace sentry 40 | -------------------------------------------------------------------------------- /project/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/sentry/sentry_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/log_level.h" 4 | 5 | #include 6 | #include 7 | 8 | using namespace godot; 9 | 10 | namespace sentry { 11 | 12 | // Represents Sentry structured log entry. 13 | class SentryLog : public RefCounted { 14 | GDCLASS(SentryLog, RefCounted); 15 | 16 | public: 17 | using LogLevel = ::sentry::LogLevel; 18 | 19 | protected: 20 | static void _bind_methods(); 21 | 22 | public: 23 | virtual LogLevel get_level() const = 0; 24 | virtual void set_level(LogLevel p_level) = 0; 25 | 26 | virtual String get_body() const = 0; 27 | virtual void set_body(const String &p_body) = 0; 28 | 29 | virtual Variant get_attribute(const String &p_name) const = 0; 30 | virtual void set_attribute(const String &p_name, const Variant &p_value) = 0; 31 | virtual void add_attributes(const Dictionary &p_attributes) = 0; 32 | virtual void remove_attribute(const String &p_name) = 0; 33 | 34 | virtual ~SentryLog() = default; 35 | }; 36 | 37 | } //namespace sentry 38 | 39 | VARIANT_ENUM_CAST(sentry::SentryLog::LogLevel); 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sentry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/sentry/android/android_log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/sentry_log.h" 4 | 5 | namespace sentry::android { 6 | 7 | class AndroidLog : public SentryLog { 8 | GDCLASS(AndroidLog, SentryLog); 9 | 10 | private: 11 | Object *android_plugin = nullptr; 12 | int32_t handle = 0; 13 | bool is_borrowed = false; 14 | 15 | protected: 16 | static void _bind_methods() {} 17 | 18 | public: 19 | virtual LogLevel get_level() const override; 20 | virtual void set_level(LogLevel p_level) override; 21 | 22 | virtual String get_body() const override; 23 | virtual void set_body(const String &p_body) override; 24 | 25 | virtual Variant get_attribute(const String &p_name) const override; 26 | virtual void set_attribute(const String &p_name, const Variant &p_value) override; 27 | virtual void add_attributes(const Dictionary &p_attributes) override; 28 | virtual void remove_attribute(const String &p_name) override; 29 | 30 | void set_as_borrowed() { is_borrowed = true; } 31 | 32 | AndroidLog(); 33 | AndroidLog(Object *p_android_plugin, int32_t p_handle); 34 | virtual ~AndroidLog() override; 35 | }; 36 | 37 | } // namespace sentry::android 38 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_disabled.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Events should not be logged for errors when the logger is disabled. 3 | 4 | 5 | signal callback_processed 6 | 7 | var _num_events: int = 0 8 | 9 | 10 | func before() -> void: 11 | SentrySDK.init(func(options: SentryOptions) -> void: 12 | options.logger_enabled = false 13 | 14 | # Make sure other limits are not interfering. 15 | options.logger_limits.events_per_frame = 88 16 | options.logger_limits.throttle_events = 88 17 | options.logger_limits.repeated_error_window_ms = 0 18 | options.logger_limits.throttle_window_ms = 0 19 | 20 | options.before_send = _before_send 21 | ) 22 | 23 | 24 | func _before_send(ev: SentryEvent) -> SentryEvent: 25 | if ev.is_crash(): 26 | # Likely processing previous crash. 27 | return ev 28 | _num_events += 1 29 | callback_processed.emit() 30 | return null 31 | 32 | 33 | func test_event_and_breadcrumb_masks() -> void: 34 | var monitor := monitor_signals(self, false) 35 | push_error("dummy-error") 36 | push_warning("dummy-warning") 37 | await assert_signal(monitor).is_not_emitted("callback_processed") 38 | 39 | assert_int(_num_events).is_equal(0) 40 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_with_masks_empty.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Events and breadcrumbs should not be logged when both "logger_event_mask" 3 | ## and "logger_breadcrumb_mask" are set to zero. 4 | 5 | 6 | signal callback_processed 7 | 8 | var _num_events: int = 0 9 | 10 | 11 | func before() -> void: 12 | SentrySDK.init(func(options: SentryOptions) -> void: 13 | options.logger_event_mask = 0 14 | options.logger_breadcrumb_mask = 0 15 | 16 | options.before_send = _before_send 17 | ) 18 | 19 | 20 | func _before_send(ev: SentryEvent) -> SentryEvent: 21 | if ev.is_crash(): 22 | # Likely processing previous crash. 23 | return ev 24 | _num_events += 1 25 | callback_processed.emit() 26 | return null 27 | 28 | 29 | ## No events or breadcrumbs should be logged for errors. 30 | ## TODO: can't verify breadcrumbs yet, maybe later. 31 | func test_event_and_breadcrumb_masks() -> void: 32 | var monitor := monitor_signals(self, false) 33 | push_error("dummy-error") 34 | push_warning("dummy-warning") 35 | await assert_signal(monitor).is_not_emitted("callback_processed") 36 | 37 | await get_tree().create_timer(0.1).timeout 38 | assert_int(_num_events).is_equal(0) 39 | -------------------------------------------------------------------------------- /project/icon.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://djdyhgbcdue6n" 6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://icon.svg" 14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/uastc_level=0 22 | compress/rdo_quality_loss=0.0 23 | compress/hdr_compression=1 24 | compress/normal_map=0 25 | compress/channel_pack=0 26 | mipmaps/generate=false 27 | mipmaps/limit=-1 28 | roughness/mode=0 29 | roughness/src_normal="" 30 | process/channel_remap/red=0 31 | process/channel_remap/green=1 32 | process/channel_remap/blue=2 33 | process/channel_remap/alpha=3 34 | process/fix_alpha_border=true 35 | process/premult_alpha=false 36 | process/normal_map_invert_y=false 37 | process/hdr_as_srgb=false 38 | process/hdr_clamp_exposure=false 39 | process/size_limit=0 40 | detect_3d/compress_to=1 41 | svg/scale=1.0 42 | editor/scale_with_editor_scale=false 43 | editor/convert_colors_with_editor_theme=false 44 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_export_plugin_android.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_editor_export_plugin_android.h" 2 | 3 | #ifdef TOOLS_ENABLED 4 | 5 | #include 6 | 7 | String SentryEditorExportPluginAndroid::_get_name() const { 8 | return "SentryAndroidExportPlugin"; 9 | } 10 | 11 | bool SentryEditorExportPluginAndroid::_supports_platform(const Ref &p_platform) const { 12 | return p_platform->get_os_name() == "Android"; 13 | } 14 | 15 | PackedStringArray SentryEditorExportPluginAndroid::_get_android_libraries(const Ref &p_platform, bool p_debug) const { 16 | PackedStringArray libs; 17 | String release_or_debug = p_debug ? "debug" : "release"; 18 | libs.append("sentry/bin/android/sentry_android_godot_plugin." + release_or_debug + ".aar"); 19 | return libs; 20 | } 21 | 22 | PackedStringArray SentryEditorExportPluginAndroid::_get_android_dependencies(const Ref &p_platform, bool p_debug) const { 23 | PackedStringArray deps; 24 | // NOTE: All dependencies below must be also updated in build.gradle.kts. 25 | deps.append("io.sentry:sentry-android:8.29.0"); 26 | return deps; 27 | } 28 | 29 | #endif // TOOLS_ENABLED 30 | -------------------------------------------------------------------------------- /project/test/suites/test_attachment.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Basic tests for the SentryAttachment class. 3 | 4 | 5 | func test_create_with_path() -> void: 6 | var attachment := SentryAttachment.create_with_path("user://logs/godot.log") 7 | attachment.filename = "logfile.txt" 8 | attachment.content_type = "text/plain" 9 | 10 | assert_array(attachment.bytes).is_empty() 11 | assert_str(attachment.path).is_equal("user://logs/godot.log") 12 | assert_str(attachment.filename).is_equal("logfile.txt") 13 | assert_str(attachment.content_type).is_equal("text/plain") 14 | 15 | 16 | func test_create_with_bytes() -> void: 17 | var contents := """ 18 | Hello, world! 19 | """ 20 | var bytes: PackedByteArray = contents.to_utf8_buffer() 21 | 22 | var attachment := SentryAttachment.create_with_bytes(bytes, "hello.txt") 23 | attachment.content_type = "text/plain" 24 | 25 | assert_array(attachment.bytes).is_not_empty() 26 | assert_str(attachment.path).is_empty() 27 | assert_str(attachment.filename).is_equal("hello.txt") 28 | assert_str(attachment.content_type).is_equal("text/plain") 29 | 30 | assert_int(attachment.bytes.size()).is_equal(bytes.size()) 31 | for i in attachment.bytes.size(): 32 | assert_int(attachment.bytes[i]).is_equal(bytes[i]) 33 | -------------------------------------------------------------------------------- /src/sentry/uuid.cpp: -------------------------------------------------------------------------------- 1 | #include "uuid.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | namespace { 8 | 9 | inline PackedByteArray _generate_uuid_v4() { 10 | PackedByteArray data; 11 | data.resize(16); 12 | std::random_device rd; 13 | std::mt19937 gen{ rd() }; 14 | std::uniform_int_distribution dist{ 0, 256 }; // Limits 15 | for (int i = 0; i < 16; i++) { 16 | data[i] = ((unsigned char)dist(gen)); 17 | } 18 | data[6] = (data[6] & 0x0F) | 0x40; // Version 4 19 | data[8] = (data[8] & 0x3F) | 0x80; // Variant 10xx 20 | return data; 21 | } 22 | 23 | inline String _uuid_to_string(const PackedByteArray &p_uuid) { 24 | char buffer[37]; 25 | std::snprintf(buffer, sizeof(buffer), 26 | "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", 27 | p_uuid[0], p_uuid[1], p_uuid[2], p_uuid[3], 28 | p_uuid[4], p_uuid[5], 29 | p_uuid[6], p_uuid[7], 30 | p_uuid[8], p_uuid[9], 31 | p_uuid[10], p_uuid[11], p_uuid[12], p_uuid[13], p_uuid[14], p_uuid[15]); 32 | return String(buffer); 33 | } 34 | 35 | } // unnamed namespace 36 | 37 | namespace sentry::uuid { 38 | 39 | String make_uuid() { 40 | return _uuid_to_string(_generate_uuid_v4()); 41 | } 42 | 43 | } // namespace sentry::uuid 44 | -------------------------------------------------------------------------------- /project/test/isolated/test_sdk_lifecycle.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test lifecycle methods. 3 | 4 | 5 | signal callback_processed 6 | 7 | 8 | func _before_send(ev: SentryEvent) -> SentryEvent: 9 | if ev.is_crash(): 10 | # Likely processing previous crash. 11 | return ev 12 | callback_processed.emit() 13 | return null 14 | 15 | 16 | ## Test manual initialization and shutdown of SDK. 17 | func test_sdk_lifecycle() -> void: 18 | monitor_signals(self, false) 19 | 20 | # SDK should be disabled at start. 21 | assert_bool(SentrySDK.is_enabled()).is_false() 22 | 23 | SentrySDK.capture_message("message not captured before SDK is initialized") 24 | await assert_signal(self).is_not_emitted("callback_processed") 25 | 26 | SentrySDK.init(func (options: SentryOptions) -> void: 27 | options.before_send = _before_send 28 | ) 29 | assert_bool(SentrySDK.is_enabled()).is_true() 30 | 31 | SentrySDK.capture_message("message captured when SDK is initialiazed") 32 | await assert_signal(self).is_emitted("callback_processed") 33 | 34 | SentrySDK.close() 35 | assert_bool(SentrySDK.is_enabled()).is_false() 36 | 37 | SentrySDK.capture_message("message not captured when SDK is closed") 38 | await assert_signal(self).is_not_emitted("callback_processed") 39 | -------------------------------------------------------------------------------- /src/sentry/sentry_breadcrumb.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_BREADCRUMB_H 2 | #define SENTRY_BREADCRUMB_H 3 | 4 | #include "sentry/level.h" 5 | #include "sentry/sentry_timestamp.h" 6 | 7 | #include 8 | 9 | using namespace godot; 10 | 11 | namespace sentry { 12 | 13 | // Represents breadcrumbs in the public API. 14 | class SentryBreadcrumb : public RefCounted { 15 | GDCLASS(SentryBreadcrumb, RefCounted); 16 | 17 | protected: 18 | static void _bind_methods(); 19 | 20 | public: 21 | static Ref create(const String &p_message = ""); 22 | 23 | virtual void set_message(const String &p_message) = 0; 24 | virtual String get_message() const = 0; 25 | 26 | virtual void set_category(const String &p_category) = 0; 27 | virtual String get_category() const = 0; 28 | 29 | virtual void set_level(sentry::Level p_level) = 0; 30 | virtual sentry::Level get_level() const = 0; 31 | 32 | virtual void set_type(const String &p_type) = 0; 33 | virtual String get_type() const = 0; 34 | 35 | virtual void set_data(const Dictionary &p_data) = 0; 36 | 37 | virtual Ref get_timestamp() = 0; 38 | 39 | virtual ~SentryBreadcrumb() = default; 40 | }; 41 | 42 | } //namespace sentry 43 | 44 | #endif // SENTRY_BREADCRUMB_H 45 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling_disabled.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test throttling should be disabled if the window is set to 0. 3 | 4 | 5 | signal callback_processed 6 | 7 | var _num_events: int = 0 8 | 9 | 10 | func before() -> void: 11 | SentrySDK.init(func(options: SentryOptions) -> void: 12 | # Setting throttle window to 0 should disable throttling. 13 | options.logger_limits.throttle_window_ms = 0 14 | options.logger_limits.throttle_events = 1 15 | # Make sure other limits are not interfering. 16 | options.logger_limits.events_per_frame = 88 17 | options.before_send = _before_send 18 | ) 19 | 20 | 21 | func _before_send(ev: SentryEvent) -> SentryEvent: 22 | if ev.is_crash(): 23 | # Likely processing previous crash. 24 | return ev 25 | _num_events += 1 26 | callback_processed.emit() 27 | return null 28 | 29 | 30 | ## All errors should be logged. 31 | func test_throttling_window_set_to_zero() -> void: 32 | monitor_signals(self, false) 33 | push_error("dummy-error 1") 34 | push_error("dummy-error 2") 35 | push_error("dummy-error 3") 36 | push_error("dummy-error 4") 37 | await assert_signal(self).is_emitted("callback_processed") 38 | await get_tree().create_timer(0.1).timeout 39 | assert_int(_num_events).is_equal(4) 40 | -------------------------------------------------------------------------------- /src/sentry/level.cpp: -------------------------------------------------------------------------------- 1 | #include "level.h" 2 | 3 | #include "sentry/logging/print.h" 4 | 5 | namespace sentry { 6 | 7 | godot::CharString level_as_cstring(Level level) { 8 | switch (level) { 9 | case Level::LEVEL_DEBUG: 10 | return "debug"; 11 | case Level::LEVEL_INFO: 12 | return "info"; 13 | case Level::LEVEL_WARNING: 14 | return "warning"; 15 | case Level::LEVEL_ERROR: 16 | return "error"; 17 | case Level::LEVEL_FATAL: 18 | return "fatal"; 19 | default: 20 | return "unknown"; 21 | } 22 | } 23 | 24 | godot::PropertyInfo make_level_enum_property(const godot::String &p_name) { 25 | return godot::PropertyInfo(godot::Variant::INT, p_name, godot::PROPERTY_HINT_ENUM, "Debug,Info,Warning,Error,Fatal"); 26 | } 27 | 28 | Level int_to_level(int p_value) { 29 | switch (p_value) { 30 | case 0: 31 | return Level::LEVEL_DEBUG; 32 | case 1: 33 | return Level::LEVEL_INFO; 34 | case 2: 35 | return Level::LEVEL_WARNING; 36 | case 3: 37 | return Level::LEVEL_ERROR; 38 | case 4: 39 | return Level::LEVEL_FATAL; 40 | default: 41 | sentry::logging::print_error("Internal Error: Unexpected SentryLevel integer value: " + godot::String::num_int64(p_value)); 42 | return Level::LEVEL_ERROR; 43 | } 44 | } 45 | 46 | } // namespace sentry 47 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_plugin.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_editor_plugin.h" 2 | 3 | #ifdef TOOLS_ENABLED 4 | 5 | #include "editor/sentry_editor_export_plugin_android.h" 6 | #include "editor/sentry_editor_export_plugin_unix.h" 7 | #include "sentry/logging/print.h" 8 | 9 | void SentryEditorPlugin::_notification(int p_what) { 10 | switch (p_what) { 11 | case NOTIFICATION_ENTER_TREE: { 12 | sentry::logging::print_debug("adding export plugins"); 13 | 14 | if (android_export_plugin.is_null()) { 15 | android_export_plugin = Ref(memnew(SentryEditorExportPluginAndroid)); 16 | } 17 | add_export_plugin(android_export_plugin); 18 | 19 | #ifndef WINDOWS_ENABLED 20 | if (unix_export_plugin.is_null()) { 21 | unix_export_plugin = Ref(memnew(SentryEditorExportPluginUnix)); 22 | } 23 | add_export_plugin(unix_export_plugin); 24 | #endif 25 | } break; 26 | case NOTIFICATION_EXIT_TREE: { 27 | if (android_export_plugin.is_valid()) { 28 | remove_export_plugin(android_export_plugin); 29 | android_export_plugin.unref(); 30 | } 31 | 32 | #ifndef WINDOWS_ENABLED 33 | if (unix_export_plugin.is_valid()) { 34 | remove_export_plugin(unix_export_plugin); 35 | unix_export_plugin.unref(); 36 | } 37 | #endif 38 | } break; 39 | } 40 | } 41 | 42 | #endif // TOOLS_ENABLED 43 | -------------------------------------------------------------------------------- /src/sentry/sentry_logger.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "sentry/log_level.h" 4 | 5 | #include 6 | 7 | using namespace godot; 8 | 9 | namespace sentry { 10 | 11 | // Public interface for Sentry structured logging. 12 | class SentryLogger : public Object { 13 | GDCLASS(SentryLogger, Object); 14 | 15 | protected: 16 | static void _bind_methods(); 17 | 18 | public: 19 | void log(LogLevel p_level, const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 20 | void trace(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 21 | void debug(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 22 | void info(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 23 | void warn(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 24 | void error(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 25 | void fatal(const String &p_body, const Array &p_params = Array(), const Dictionary &p_attributes = Dictionary()); 26 | 27 | SentryLogger(); 28 | }; 29 | 30 | } // namespace sentry 31 | -------------------------------------------------------------------------------- /src/sentry/sentry_breadcrumb.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_breadcrumb.h" 2 | 3 | #include "sentry/util/simple_bind.h" 4 | #include "sentry_sdk.h" // Needed for VariantCaster 5 | 6 | namespace sentry { 7 | 8 | Ref SentryBreadcrumb::create(const String &p_message) { 9 | ERR_FAIL_NULL_V(SentrySDK::get_singleton(), nullptr); 10 | auto internal_sdk = SentrySDK::get_singleton()->get_internal_sdk(); 11 | Ref instance = internal_sdk->create_breadcrumb(); 12 | if (!p_message.is_empty()) { 13 | instance->set_message(p_message); 14 | } 15 | return instance; 16 | } 17 | 18 | void SentryBreadcrumb::_bind_methods() { 19 | ClassDB::bind_static_method("SentryBreadcrumb", D_METHOD("create", "message"), &SentryBreadcrumb::create, DEFVAL(String())); 20 | 21 | BIND_PROPERTY_SIMPLE(SentryBreadcrumb, Variant::STRING, message); 22 | BIND_PROPERTY_SIMPLE(SentryBreadcrumb, Variant::STRING, category); 23 | BIND_PROPERTY(SentryBreadcrumb, sentry::make_level_enum_property("level"), set_level, get_level); 24 | BIND_PROPERTY_SIMPLE(SentryBreadcrumb, Variant::STRING, type); 25 | 26 | ClassDB::bind_method(D_METHOD("set_data", "data"), &SentryBreadcrumb::set_data); 27 | ClassDB::bind_method(D_METHOD("get_timestamp"), &SentryBreadcrumb::get_timestamp); 28 | } 29 | 30 | } //namespace sentry 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project-related 2 | project/addons/gdUnit4 3 | project/addons/sentry/* 4 | !project/addons/sentry/user_feedback/ 5 | project/export_presets.cfg 6 | project/.vscode/ 7 | project/reports/ 8 | project/android/ 9 | project/.godot/ 10 | 11 | # Export related 12 | exports/* 13 | !exports/export_presets.cfg 14 | 15 | integration_tests/results/ 16 | 17 | # Source tree 18 | src/gen/ 19 | 20 | # Cocoa: downloaded automatically. 21 | modules/sentry-cocoa/ 22 | 23 | # Android plugin 24 | .gradle/ 25 | .idea/ 26 | build/ 27 | local.properties 28 | *.log 29 | 30 | # Objects 31 | *.o 32 | *.os 33 | *.so 34 | *.obj 35 | *.bc 36 | *.pyc 37 | *.dblite 38 | *.pdb 39 | *.lib 40 | *.config 41 | *.creator 42 | *.creator.user 43 | *.files 44 | *.includes 45 | *.idb 46 | 47 | # Vim temp files 48 | *.swo 49 | *.swp 50 | 51 | # Scons 52 | .sconf_temp 53 | .sconsign.dblite 54 | .scons_node_count 55 | 56 | # Misc 57 | .DS_Store 58 | ~$* 59 | *~ 60 | 61 | # Windows image file caches 62 | Thumbs.db 63 | ehthumbs.db 64 | 65 | # Windows folder config file 66 | Desktop.ini 67 | 68 | # Visual Studio and Visual Studio Code workspace folders 69 | /.vs 70 | /.vscode 71 | *.code-workspace 72 | 73 | # compile commands (https://clang.llvm.org/docs/JSONCompilationDatabase.html) 74 | compile_commands.json 75 | 76 | # clang cache 77 | .cache 78 | -------------------------------------------------------------------------------- /src/sentry/sentry_user.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_USER_H 2 | #define SENTRY_USER_H 3 | 4 | #include 5 | #include 6 | 7 | using namespace godot; 8 | 9 | namespace sentry { 10 | 11 | class SentryUser : public RefCounted { 12 | GDCLASS(SentryUser, RefCounted); 13 | 14 | private: 15 | String id; 16 | String username; 17 | String email; 18 | String ip_address; 19 | 20 | protected: 21 | static void _bind_methods(); 22 | 23 | String _to_string() const; 24 | 25 | public: 26 | static Ref create_default(); 27 | 28 | void set_id(const String &p_user_id) { id = p_user_id; } 29 | String get_id() const { return id; } 30 | 31 | void set_username(const String &p_username) { username = p_username; } 32 | String get_username() const { return username; } 33 | 34 | void set_email(const String &p_email) { email = p_email; } 35 | String get_email() const { return email; } 36 | 37 | void set_ip_address(const String &p_ip_address) { ip_address = p_ip_address; } 38 | String get_ip_address() const { return ip_address; } 39 | 40 | void infer_ip_address() { ip_address = "{{auto}}"; } 41 | 42 | bool is_empty() const; 43 | 44 | void generate_new_id(); 45 | 46 | Ref duplicate(); 47 | }; 48 | 49 | } // namespace sentry 50 | 51 | #endif // SENTRY_USER_H 52 | -------------------------------------------------------------------------------- /src/sentry/native/native_breadcrumb.h: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_BREADCRUMB_H 2 | #define NATIVE_BREADCRUMB_H 3 | 4 | #include "sentry/sentry_breadcrumb.h" 5 | 6 | #include 7 | 8 | namespace sentry::native { 9 | 10 | class NativeBreadcrumb : public SentryBreadcrumb { 11 | GDCLASS(NativeBreadcrumb, SentryBreadcrumb); 12 | 13 | private: 14 | sentry_value_t native_crumb; 15 | 16 | protected: 17 | static void _bind_methods() {} 18 | 19 | public: 20 | _FORCE_INLINE_ sentry_value_t get_native_breadcrumb() { return native_crumb; } 21 | 22 | virtual void set_message(const String &p_message) override; 23 | virtual String get_message() const override; 24 | 25 | virtual void set_category(const String &p_category) override; 26 | virtual String get_category() const override; 27 | 28 | virtual void set_level(sentry::Level p_level) override; 29 | virtual sentry::Level get_level() const override; 30 | 31 | virtual void set_type(const String &p_type) override; 32 | virtual String get_type() const override; 33 | 34 | virtual void set_data(const Dictionary &p_data) override; 35 | 36 | virtual Ref get_timestamp() override; 37 | 38 | NativeBreadcrumb(sentry_value_t p_native_crumb); 39 | NativeBreadcrumb(); 40 | virtual ~NativeBreadcrumb() override; 41 | }; 42 | 43 | } //namespace sentry::native 44 | 45 | #endif // NATIVE_BREADCRUMB_H 46 | -------------------------------------------------------------------------------- /src/sentry/android/android_breadcrumb.h: -------------------------------------------------------------------------------- 1 | #ifndef ANDROID_BREADCRUMB_H 2 | #define ANDROID_BREADCRUMB_H 3 | 4 | #include "sentry/sentry_breadcrumb.h" 5 | 6 | namespace sentry::android { 7 | 8 | class AndroidBreadcrumb : public SentryBreadcrumb { 9 | GDCLASS(AndroidBreadcrumb, SentryBreadcrumb); 10 | 11 | private: 12 | Object *android_plugin = nullptr; 13 | int32_t handle = 0; 14 | 15 | protected: 16 | static void _bind_methods() {} 17 | 18 | public: 19 | int32_t get_handle() { return handle; } 20 | 21 | virtual void set_message(const String &p_message) override; 22 | virtual String get_message() const override; 23 | 24 | virtual void set_category(const String &p_category) override; 25 | virtual String get_category() const override; 26 | 27 | virtual void set_level(sentry::Level p_level) override; 28 | virtual sentry::Level get_level() const override; 29 | 30 | virtual void set_type(const String &p_type) override; 31 | virtual String get_type() const override; 32 | 33 | virtual void set_data(const Dictionary &p_data) override; 34 | 35 | virtual Ref get_timestamp() override; 36 | 37 | AndroidBreadcrumb() = default; 38 | AndroidBreadcrumb(Object *p_android_plugin, int32_t p_breadcrumb_handle); 39 | virtual ~AndroidBreadcrumb() override; 40 | }; 41 | 42 | } //namespace sentry::android 43 | 44 | #endif // ANDROID_BREADCRUMB_H 45 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_breadcrumb.h: -------------------------------------------------------------------------------- 1 | #ifndef COCOA_BREADCRUMB_H 2 | #define COCOA_BREADCRUMB_H 3 | 4 | #include "sentry/cocoa/cocoa_includes.h" 5 | #include "sentry/sentry_breadcrumb.h" 6 | 7 | namespace sentry::cocoa { 8 | 9 | class CocoaBreadcrumb : public SentryBreadcrumb { 10 | GDCLASS(CocoaBreadcrumb, SentryBreadcrumb); 11 | 12 | private: 13 | objc::SentryBreadcrumb *cocoa_breadcrumb = nullptr; 14 | 15 | protected: 16 | static void _bind_methods() {} 17 | 18 | public: 19 | _FORCE_INLINE_ objc::SentryBreadcrumb *get_cocoa_breadcrumb() const { return cocoa_breadcrumb; } 20 | 21 | virtual void set_message(const String &p_message) override; 22 | virtual String get_message() const override; 23 | 24 | virtual void set_category(const String &p_category) override; 25 | virtual String get_category() const override; 26 | 27 | virtual void set_level(sentry::Level p_level) override; 28 | virtual sentry::Level get_level() const override; 29 | 30 | virtual void set_type(const String &p_type) override; 31 | virtual String get_type() const override; 32 | 33 | virtual void set_data(const Dictionary &p_data) override; 34 | 35 | virtual Ref get_timestamp() override; 36 | 37 | CocoaBreadcrumb(); 38 | CocoaBreadcrumb(objc::SentryBreadcrumb *p_cocoa_breadcrumb); 39 | }; 40 | 41 | } //namespace sentry::cocoa 42 | 43 | #endif // COCOA_BREADCRUMB_H 44 | -------------------------------------------------------------------------------- /scripts/bump-version.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory = $false)][String]$oldVersion, 3 | [Parameter(Mandatory = $true)][String]$newVersion 4 | ) 5 | Set-StrictMode -Version latest 6 | 7 | $sconsFile = "$PSScriptRoot/../SConstruct" 8 | $content = Get-Content $sconsFile 9 | $content -replace 'VERSION = ".*"', ('VERSION = "' + $newVersion + '"') | Out-File $sconsFile 10 | 11 | # Check that the version was updated. 12 | if ("$content" -eq "$(Get-Content $sconsFile)") { 13 | $versionInFile = [regex]::Match("$content", 'VERSION = "([^"]+)"').Groups[1].Value 14 | if ("$versionInFile" -ne "$newVersion") 15 | { 16 | Throw "Failed to update version in $sconsFile - the content didn't change. The version found in the file is '$versionInFile'." 17 | } 18 | } 19 | 20 | $projectFile = "$PSScriptRoot/../project/project.godot" 21 | $projectContent = Get-Content $projectFile 22 | $projectContent -replace 'config/version=".*"', ('config/version="' + $newVersion + '"') | Out-File $projectFile 23 | 24 | if ("$projectContent" -eq "$(Get-Content $projectFile)") { 25 | $versionInFile = [regex]::Match("$projectContent", 'config/version="([^"]+)"').Groups[1].Value 26 | if ("$versionInFile" -ne "$newVersion") 27 | { 28 | Throw "Failed to update version in $projectFile - the content didn't change. The version found in the file is '$versionInFile'." 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/sentry/native/native_util.h: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_UTIL_H 2 | #define NATIVE_UTIL_H 3 | 4 | #include "godot_cpp/core/defs.hpp" 5 | #include "sentry/level.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace godot; 13 | 14 | namespace sentry::native { 15 | 16 | // Convert Godot Variant to sentry_value_t. 17 | sentry_value_t variant_to_sentry_value(const Variant &p_variant, int p_depth = 0); 18 | 19 | // Convert PackedStringArray to sentry_value_t (as a list). 20 | sentry_value_t strings_to_sentry_list(const PackedStringArray &p_strings); 21 | 22 | sentry_level_t level_to_native(Level p_level); 23 | Level native_to_level(sentry_level_t p_native_level); 24 | 25 | // TODO: move this to level.h 26 | CharString level_to_cstring(Level p_level); 27 | Level cstring_to_level(const CharString &p_cstring); 28 | 29 | _FORCE_INLINE_ void sentry_value_set_or_remove_string_by_key(sentry_value_t value, const char *k, const String &v) { 30 | if (v.is_empty()) { 31 | sentry_value_remove_by_key(value, k); 32 | } else { 33 | sentry_value_set_by_key(value, k, sentry_value_new_string(v.utf8())); 34 | } 35 | } 36 | 37 | sentry_value_t variant_to_attribute(const Variant &p_value); 38 | 39 | } //namespace sentry::native 40 | 41 | #endif // NATIVE_UTIL_H 42 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_events_per_frame.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test "events_per_frame" error logger limit. 3 | 4 | 5 | signal callback_processed 6 | 7 | var _num_events: int = 0 8 | 9 | 10 | func before() -> void: 11 | SentrySDK.init(func(options: SentryOptions) -> void: 12 | # Only one error is allowed to be logged as event per processed frame. 13 | options.logger_limits.events_per_frame = 1 14 | # Make sure other limits are not interfering. 15 | options.logger_limits.repeated_error_window_ms = 0 16 | options.logger_limits.throttle_events = 88 17 | options.before_send = _before_send 18 | ) 19 | 20 | 21 | func _before_send(ev: SentryEvent) -> SentryEvent: 22 | if ev.is_crash(): 23 | # Likely processing previous crash. 24 | return ev 25 | _num_events += 1 26 | callback_processed.emit() 27 | return null 28 | 29 | 30 | ## Only one error should be logged within 1 processed frame. 31 | func test_events_per_frame_limit() -> void: 32 | # Wait for special startup limits to expire. 33 | while Engine.get_process_frames() < 10: 34 | await get_tree().process_frame 35 | 36 | monitor_signals(self, false) 37 | 38 | push_error("dummy-error") 39 | push_error("dummy-error") 40 | push_error("dummy-error") 41 | 42 | await assert_signal(self).is_emitted("callback_processed") 43 | await get_tree().create_timer(0.1).timeout 44 | assert_int(_num_events).is_equal(1) 45 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_breadcrumbs.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Verify Godot errors are properly adding breadcrumbs. 3 | 4 | 5 | func init_sdk() -> void: 6 | SentrySDK.init(func(options: SentryOptions) -> void: 7 | options.logger_messages_as_breadcrumbs = true 8 | ) 9 | 10 | 11 | func test_logger_warnings_and_prints_create_breadcrumbs() -> void: 12 | # Wait a small amount of time to dodge initial device events on Android. 13 | await get_tree().create_timer(0.5).timeout 14 | 15 | # We expect print() and push_warning() to become breadcrumbs and not create events. 16 | print("Debug message") 17 | push_warning("Warning message") 18 | push_error("Final error message") 19 | 20 | var json: String = await wait_for_captured_event_json() 21 | assert_int(captured_events.size()).is_equal(1).override_failure_message("expected a single event") 22 | 23 | assert_json(json).describe("print() should appear as the pre-last breadcrumb") \ 24 | .at("/breadcrumbs/-2") \ 25 | .must_contain("message", "Debug message") \ 26 | .must_contain("level", "info") \ 27 | .must_contain("category", "log") \ 28 | .verify() 29 | 30 | assert_json(json).describe("Warning should appear as the last breadcrumb") \ 31 | .at("/breadcrumbs/-1") \ 32 | .must_contain("message", "Warning message") \ 33 | .must_contain("level", "warning") \ 34 | .must_contain("category", "error") \ 35 | .verify() 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 🎁 Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: Version to release 8 | required: true 9 | force: 10 | description: Force a release even when there are release-blockers (optional) 11 | required: false 12 | 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | 17 | jobs: 18 | job_release: 19 | runs-on: ubuntu-latest 20 | name: "Release a new version: ${{ github.event.inputs.version }}" 21 | steps: 22 | - name: Get auth token 23 | id: token 24 | uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 25 | with: 26 | app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} 27 | private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} 28 | 29 | - name: Check out current commit (${{ github.sha }}) 30 | uses: actions/checkout@v4 31 | with: 32 | token: ${{ steps.token.outputs.token }} 33 | fetch-depth: 0 34 | 35 | - name: Prepare release ${{ github.event.inputs.version }} 36 | uses: getsentry/action-prepare-release@v1 37 | env: 38 | GITHUB_TOKEN: ${{ steps.token.outputs.token }} 39 | with: 40 | version: ${{ github.event.inputs.version }} 41 | force: ${{ github.event.inputs.force }} 42 | -------------------------------------------------------------------------------- /scripts/post-release.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory = $false)][String]$oldVersion, 3 | [Parameter(Mandatory = $true)][String]$newVersion 4 | ) 5 | Set-StrictMode -Version latest 6 | 7 | git checkout main 8 | 9 | if ($newVersion -match '^(?\d+)\.(?\d+)\.(?\d+)(?.*)?$') { 10 | $major = [int]$matches['major'] 11 | $minor = [int]$matches['minor'] 12 | $patch = [int]$matches['patch'] 13 | $status = $matches['status'] 14 | 15 | if ($status -match '^-(?[a-zA-Z]+)\.?(?\d+)?$') { 16 | # Increment prerelease version 17 | $prerelease = $matches['prerelease'] 18 | $prereleaseNum = [int]$matches['prereleaseNum'] 19 | $prereleaseNum += 1 20 | $status = "-$prerelease.$prereleaseNum" 21 | } else { 22 | # Increment minor version, reset patch version, and add -dev prerelease status 23 | $minor += 1 24 | $patch = 0 25 | $status = '-dev' 26 | } 27 | 28 | $nextVersion = "$major.$minor.$patch$status" 29 | 30 | & 'pwsh.\scripts\bump-version.ps1' -prevVersion $newVersion -newVersion $nextVersion 31 | 32 | git diff --quiet 33 | if ($LASTEXITCODE -ne 0) { 34 | git commit -anm 'meta: Bump new development version' 35 | git pull --rebase 36 | git push 37 | } 38 | 39 | } else {` 40 | Throw "Failed to find version in $sconsFile" 41 | } 42 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. For more details, visit 12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /src/sentry/disabled/disabled_breadcrumb.h: -------------------------------------------------------------------------------- 1 | #ifndef DISABLED_BREADCRUMB_H 2 | #define DISABLED_BREADCRUMB_H 3 | 4 | #include "sentry/level.h" 5 | #include "sentry/sentry_breadcrumb.h" 6 | 7 | namespace sentry { 8 | 9 | class DisabledBreadcrumb : public SentryBreadcrumb { 10 | GDCLASS(DisabledBreadcrumb, SentryBreadcrumb); 11 | 12 | private: 13 | String message; 14 | String category; 15 | sentry::Level level = sentry::Level::LEVEL_INFO; 16 | String type; 17 | Dictionary data; 18 | 19 | protected: 20 | static void _bind_methods() {} 21 | 22 | public: 23 | virtual void set_message(const String &p_message) override { message = p_message; } 24 | virtual String get_message() const override { return message; } 25 | 26 | virtual void set_category(const String &p_category) override { category = p_category; } 27 | virtual String get_category() const override { return category; } 28 | 29 | virtual void set_level(sentry::Level p_level) override { level = p_level; } 30 | virtual sentry::Level get_level() const override { return level; } 31 | 32 | virtual void set_type(const String &p_type) override { type = p_type; } 33 | virtual String get_type() const override { return type; } 34 | 35 | virtual void set_data(const Dictionary &p_data) override { data = p_data; } 36 | 37 | virtual Ref get_timestamp() override { return memnew(SentryTimestamp); } 38 | }; 39 | 40 | } // namespace sentry 41 | 42 | #endif // DISABLED_BREADCRUMB_H 43 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling_on_startup.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test error logger throttling limits early in the app lifecycle. 3 | ## 4 | ## Note: During early app lifecycle special relaxed limits are in effect. 5 | 6 | 7 | signal callback_processed 8 | 9 | var _num_events: int = 0 10 | 11 | 12 | func before() -> void: 13 | SentrySDK.init(func(options: SentryOptions) -> void: 14 | options.logger_limits.throttle_events = 2 15 | options.logger_limits.throttle_window_ms = 100000 16 | # Make sure other limits are not interfering. 17 | options.logger_limits.events_per_frame = 88 18 | options.logger_limits.repeated_error_window_ms = 0 19 | options.before_send = _before_send 20 | ) 21 | 22 | 23 | func _before_send(ev: SentryEvent) -> SentryEvent: 24 | if ev.is_crash(): 25 | # Likely processing previous crash. 26 | return ev 27 | _num_events += 1 28 | callback_processed.emit() 29 | return null 30 | 31 | 32 | ## Only two errors should be logged within the assigned time window. 33 | func test_throttling_limits() -> void: 34 | monitor_signals(self, false) 35 | 36 | # Ensure running early. 37 | assert_int(Engine.get_process_frames()).is_less(10) 38 | 39 | push_error("dummy-error 1") 40 | push_error("dummy-error 2") 41 | push_error("dummy-error 3") 42 | push_error("dummy-error 4") 43 | push_error("dummy-error 5") 44 | 45 | await get_tree().process_frame # allow all events to process 46 | assert_int(_num_events).is_equal(5) 47 | -------------------------------------------------------------------------------- /.github/workflows/update-deps.yml: -------------------------------------------------------------------------------- 1 | name: Update Dependencies 2 | 3 | on: 4 | # Run every day. 5 | schedule: 6 | - cron: "0 3 * * *" 7 | # Allow a manual trigger to be able to run the update when there are new dependencies or after a PR merge to resolve CHANGELOG conflicts. 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write # needed to add commits to branches 12 | pull-requests: write 13 | actions: write # needed for cancelling workflow runs 14 | 15 | jobs: 16 | deps: 17 | name: ${{ matrix.name }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - name: Native SDK 23 | path: modules/sentry-native 24 | - name: Cocoa SDK 25 | path: modules/sentry-cocoa.properties 26 | - name: gdUnit 4 27 | path: modules/gdUnit4 28 | uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 29 | with: 30 | name: ${{ matrix.name }} 31 | path: ${{ matrix.path }} 32 | pr-strategy: update 33 | changelog-entry: ${{ matrix.path != 'modules/gdUnit4' }} 34 | secrets: 35 | api-token: ${{ secrets.CI_DEPLOY_KEY }} 36 | 37 | android: 38 | name: Sentry Android 39 | uses: getsentry/github-workflows/.github/workflows/updater.yml@v2 40 | with: 41 | name: Sentry Android 42 | path: scripts/android-version.ps1 43 | pr-strategy: update 44 | secrets: 45 | api-token: ${{ secrets.CI_DEPLOY_KEY }} 46 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_includes.h: -------------------------------------------------------------------------------- 1 | #ifndef COCOA_INCLUDES_H 2 | #define COCOA_INCLUDES_H 3 | 4 | #ifdef __OBJC__ 5 | 6 | #import 7 | #ifdef IOS_ENABLED 8 | #import 9 | #endif 10 | #import 11 | 12 | namespace objc { 13 | 14 | // Type aliases for Cocoa SDK types to avoid naming conflicts 15 | // with C++ extension types. The objc:: namespace distinguishes original 16 | // Cocoa types from types defined elsewhere. 17 | 18 | using SentryOptions = ::SentryOptions; 19 | using SentrySDK = ::SentrySDK; 20 | using SentryEvent = ::SentryEvent; 21 | using SentryUser = ::SentryUser; 22 | using SentryLevel = ::SentryLevel; 23 | using SentryBreadcrumb = ::SentryBreadcrumb; 24 | using SentryId = ::SentryId; 25 | using SentryScope = ::SentryScope; 26 | using SentryAttachment = ::SentryAttachment; 27 | using SentryMessage = ::SentryMessage; 28 | using SentryException = ::SentryException; 29 | using SentryStacktrace = ::SentryStacktrace; 30 | using SentryFrame = ::SentryFrame; 31 | using SentryThread = ::SentryThread; 32 | using SentryFeedback = ::SentryFeedback; 33 | using SentryLog = ::SentryLog; 34 | 35 | } // namespace objc 36 | 37 | #else // C++ context 38 | 39 | // In C++ context, make objc::SentryEvent an alias to void 40 | namespace objc { 41 | 42 | using SentryEvent = void; 43 | using SentryBreadcrumb = void; 44 | using SentryLog = void; 45 | 46 | } // namespace objc 47 | 48 | #endif // __OBJC__ 49 | 50 | #endif // COCOA_INCLUDES_H 51 | -------------------------------------------------------------------------------- /.github/workflows/static_checks.yml: -------------------------------------------------------------------------------- 1 | name: 🔎 Static checks 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | static-checks: 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 15 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: false # don't initialize submodules automatically 18 | 19 | - name: Prepare testing 20 | uses: ./.github/actions/prepare-testing 21 | with: 22 | arch: x86_64 23 | 24 | - name: Check code style 25 | uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 26 | 27 | - name: Check class documentation 28 | if: success() || failure() 29 | shell: bash 30 | run: | 31 | pwsh ./scripts/update-doc-classes.ps1 32 | changed=$(git diff --name-only doc_classes/) 33 | if [[ -n $changed ]]; then 34 | for file in $changed; do 35 | echo "::error file=$file::Class documentation needs to be updated." 36 | echo "::group::Diff" 37 | git diff $file 38 | echo "::endgroup::" 39 | done 40 | echo "::notice title=Tip::Run ./scripts/update-doc-classes.ps1 to update XML files in doc_classes directory before adding corrections." 41 | exit 1 42 | else 43 | echo "::notice::Class documentation is up-to-date." 44 | fi 45 | -------------------------------------------------------------------------------- /project/desktop.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://uwd7dms6675q"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://dyoaec2d7uung" path="res://views/enrich_events.tscn" id="1_uh226"] 4 | [ext_resource type="PackedScene" uid="uid://bxi26vu5tlqas" path="res://views/capture_events.tscn" id="2_qcmjx"] 5 | [ext_resource type="PackedScene" uid="uid://dllqhtd731wtc" path="res://views/output_pane.tscn" id="3_uh226"] 6 | 7 | [node name="Desktop" type="CanvasLayer"] 8 | 9 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 10 | anchors_preset = 15 11 | anchor_right = 1.0 12 | anchor_bottom = 1.0 13 | grow_horizontal = 2 14 | grow_vertical = 2 15 | 16 | [node name="Columns" type="HBoxContainer" parent="VBoxContainer"] 17 | layout_mode = 2 18 | theme_override_constants/separation = 0 19 | 20 | [node name="Spacer" type="Control" parent="VBoxContainer/Columns"] 21 | custom_minimum_size = Vector2(64, 0) 22 | layout_mode = 2 23 | 24 | [node name="EnrichEvents" parent="VBoxContainer/Columns" instance=ExtResource("1_uh226")] 25 | layout_mode = 2 26 | 27 | [node name="Spacer2" type="Control" parent="VBoxContainer/Columns"] 28 | custom_minimum_size = Vector2(64, 0) 29 | layout_mode = 2 30 | 31 | [node name="CaptureEvents" parent="VBoxContainer/Columns" instance=ExtResource("2_qcmjx")] 32 | layout_mode = 2 33 | 34 | [node name="Spacer3" type="Control" parent="VBoxContainer/Columns"] 35 | custom_minimum_size = Vector2(64, 0) 36 | layout_mode = 2 37 | 38 | [node name="OutputPane" parent="VBoxContainer" instance=ExtResource("3_uh226")] 39 | layout_mode = 2 40 | -------------------------------------------------------------------------------- /project/addons/sentry/user_feedback/user_feedback_gui.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://d3cll30toja8f"] 2 | 3 | [ext_resource type="Script" uid="uid://b7c0cq2oneiwv" path="res://addons/sentry/user_feedback/user_feedback_gui.gd" id="1_jugy2"] 4 | [ext_resource type="PackedScene" uid="uid://bdn5fqm81rhy6" path="res://addons/sentry/user_feedback/user_feedback_form.tscn" id="1_t8jgq"] 5 | [ext_resource type="Theme" uid="uid://bw0anqwp7xj8t" path="res://addons/sentry/user_feedback/sentry_theme.tres" id="1_u3uht"] 6 | 7 | [node name="UserFeedbackGUI" type="Container"] 8 | anchors_preset = 15 9 | anchor_right = 1.0 10 | anchor_bottom = 1.0 11 | grow_horizontal = 2 12 | grow_vertical = 2 13 | mouse_filter = 0 14 | theme = ExtResource("1_u3uht") 15 | script = ExtResource("1_jugy2") 16 | 17 | [node name="ColorRect" type="ColorRect" parent="."] 18 | layout_mode = 2 19 | size_flags_horizontal = 3 20 | size_flags_vertical = 3 21 | mouse_filter = 1 22 | color = Color(0, 0, 0, 0.22745098) 23 | 24 | [node name="UserFeedbackForm" parent="." instance=ExtResource("1_t8jgq")] 25 | unique_name_in_owner = true 26 | layout_mode = 2 27 | 28 | [node name="SubmitButton" parent="UserFeedbackForm/MarginContainer/VBoxContainer/Actions" index="1"] 29 | theme_type_variation = &"ActionButton" 30 | 31 | [connection signal="feedback_cancelled" from="UserFeedbackForm" to="." method="_on_user_feedback_form_feedback_cancelled"] 32 | [connection signal="feedback_submitted" from="UserFeedbackForm" to="." method="_on_user_feedback_form_feedback_submitted"] 33 | 34 | [editable path="UserFeedbackForm"] 35 | -------------------------------------------------------------------------------- /project/test/isolated/test_logger_with_masks.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Events and breadcrumbs should be logged when "logger_event_mask" and 3 | ## "logger_breadcrumb_mask" are configured to include all categories. 4 | 5 | 6 | signal callback_processed 7 | 8 | var _num_events: int = 0 9 | 10 | 11 | func before() -> void: 12 | SentrySDK.init(func(options: SentryOptions) -> void: 13 | var mask = SentryOptions.MASK_ERROR | SentryOptions.MASK_SCRIPT | SentryOptions.MASK_SHADER | SentryOptions.MASK_WARNING 14 | options.logger_event_mask = mask 15 | options.logger_breadcrumb_mask = mask 16 | 17 | # Make sure other limits are not interfering. 18 | options.logger_limits.events_per_frame = 88 19 | options.logger_limits.throttle_events = 88 20 | options.logger_limits.repeated_error_window_ms = 0 21 | options.logger_limits.throttle_window_ms = 0 22 | 23 | options.before_send = _before_send 24 | ) 25 | 26 | 27 | func _before_send(ev: SentryEvent) -> SentryEvent: 28 | if ev.is_crash(): 29 | # Likely processing previous crash. 30 | return ev 31 | _num_events += 1 32 | callback_processed.emit() 33 | return null 34 | 35 | 36 | ## Both events or breadcrumbs should be logged for error and warning. 37 | ## TODO: can't verify breadcrumbs yet, maybe later. 38 | func test_event_and_breadcrumb_masks() -> void: 39 | var monitor := monitor_signals(self, false) 40 | push_error("dummy-error") 41 | push_warning("dummy-warning") 42 | await assert_signal(monitor).is_emitted("callback_processed") 43 | 44 | await get_tree().create_timer(0.1).timeout 45 | assert_int(_num_events).is_equal(2) 46 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_repeating_error_window.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test "repeated_error_window_ms" error logger limit. 3 | 4 | 5 | signal callback_processed 6 | 7 | var _num_events: int = 0 8 | 9 | 10 | func before() -> void: 11 | SentrySDK.init(func(options: SentryOptions) -> void: 12 | # Ignore duplicate errors within 1 second window. 13 | options.logger_limits.repeated_error_window_ms = 1000 14 | # Make sure other limits are not interfering. 15 | options.logger_limits.events_per_frame = 88 16 | options.logger_limits.throttle_events = 88 17 | options.before_send = _before_send 18 | ) 19 | 20 | 21 | func _before_send(ev: SentryEvent) -> SentryEvent: 22 | if ev.is_crash(): 23 | # Likely processing previous crash. 24 | return ev 25 | _num_events += 1 26 | callback_processed.emit() 27 | return null 28 | 29 | 30 | ## Only one error should be logged within 1 second time window, and another one after 1 second passes. 31 | func test_repeating_error_window_limit() -> void: 32 | # Wait for special startup limits to expire. 33 | while Engine.get_process_frames() < 10: 34 | await get_tree().process_frame 35 | 36 | monitor_signals(self, false) 37 | 38 | push_error("dummy-error") 39 | push_error("dummy-error") 40 | await assert_signal(self).is_emitted("callback_processed") 41 | assert_int(_num_events).is_equal(1) 42 | 43 | # Wait for window to expire. 44 | await get_tree().create_timer(1.5).timeout 45 | 46 | push_error("dummy-error") 47 | push_error("dummy-error") 48 | await assert_signal(self).is_emitted("callback_processed") 49 | assert_int(_num_events).is_equal(2) 50 | -------------------------------------------------------------------------------- /project/test/suites/test_sentry_user.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Test SentryUser class. 3 | 4 | 5 | func test_sentry_user_properties() -> void: 6 | var user := SentryUser.new() 7 | user.id = "custom_id" 8 | user.email = "bob@example.com" 9 | user.username = "bob" 10 | user.ip_address = "127.0.0.1" 11 | 12 | assert_str(user.id).is_equal("custom_id") 13 | assert_str(user.email).is_equal("bob@example.com") 14 | assert_str(user.username).is_equal("bob") 15 | assert_str(user.ip_address).is_equal("127.0.0.1") 16 | 17 | 18 | ## SentryUser IP address should contain correct value when IP address is inferred. 19 | func test_sentry_user_ip_inferring() -> void: 20 | var user := SentryUser.new() 21 | user.infer_ip_address() 22 | assert_str(user.ip_address).is_equal("{{auto}}") 23 | 24 | 25 | ## SentryUser ID generation should produce a unique ID. 26 | func test_sentry_user_id_generation() -> void: 27 | var user := SentryUser.new() 28 | 29 | user.generate_new_id() 30 | var id1 := user.id 31 | user.generate_new_id() 32 | var id2 := user.id 33 | 34 | assert_int(id1.length()).is_greater(4) 35 | assert_int(id2.length()).is_greater(4) 36 | assert_str(id2).is_not_equal(id1).override_failure_message("Newly-generated ID should be different") 37 | 38 | 39 | func test_default_user_id() -> void: 40 | var default1 := SentryUser.create_default() 41 | var default2 := SentryUser.create_default() 42 | 43 | assert_str(default1.id).is_not_empty() 44 | assert_str(default2.id).is_not_empty() 45 | assert_str(default1.id).is_equal(default2.id).override_failure_message("Default user instance should contain the same persisted ID (unique per installation)") 46 | -------------------------------------------------------------------------------- /project/cli/android_cli_adapter.gd: -------------------------------------------------------------------------------- 1 | class_name AndroidCLIAdapter 2 | extends RefCounted 3 | ## Adapter class that converts Android intent extras into CLI-style arguments. 4 | ## 5 | ## Expects Android intent extras as key-value pairs: [br] 6 | ## arg0: name of the command to run. [br] 7 | ## arg1: first argument to the command. [br] 8 | ## arg2: second argument, etc... [br] 9 | 10 | 11 | # Reads Android intent extras and returns CLI-style arguments as PackedStringArray. 12 | # Supports --es command and --es arg0, arg1, arg2... 13 | static func get_command_argv() -> PackedStringArray: 14 | var rv := PackedStringArray() 15 | var extras: Dictionary = _get_android_intent_extras() 16 | 17 | for i in range(10): 18 | var key := "arg%d" % i 19 | if extras.has(key): 20 | rv.append(extras[key]) 21 | return rv 22 | 23 | 24 | # Returns intent extras from AndroidRuntime (strings-only). 25 | static func _get_android_intent_extras() -> Dictionary: 26 | if not Engine.has_singleton("AndroidRuntime"): 27 | return {} 28 | 29 | var android_runtime = Engine.get_singleton("AndroidRuntime") 30 | var activity = android_runtime.getActivity() 31 | if not activity: 32 | return {} 33 | var intent = activity.getIntent() 34 | if not intent: 35 | return {} 36 | var extras = intent.getExtras() 37 | if not extras: 38 | return {} 39 | 40 | # Convert Java map to Godot Dictionary 41 | var keys = extras.keySet().toArray() 42 | var rv: Dictionary = {} 43 | for i in range(keys.size()): 44 | var key: String = keys[i].toString() 45 | var raw_value = extras.get(key) 46 | var value: String = raw_value.toString() if raw_value != null else "" 47 | rv[key] = value 48 | 49 | return rv 50 | -------------------------------------------------------------------------------- /src/sentry/godot_error_types.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGER_ERROR_TYPES_H 2 | #define LOGGER_ERROR_TYPES_H 3 | 4 | #include "sentry/level.h" 5 | #include "sentry/log_level.h" 6 | 7 | #include 8 | #include 9 | 10 | namespace sentry { 11 | 12 | using GodotErrorType = godot::Logger::ErrorType; 13 | 14 | // Enum used with bitwise operations to represent the set of Godot error types that the Sentry logger should capture. 15 | enum GodotErrorMask { 16 | MASK_NONE = 0, 17 | MASK_ERROR = (1 << int(GodotErrorType::ERROR_TYPE_ERROR)), 18 | MASK_WARNING = (1 << int(GodotErrorType::ERROR_TYPE_WARNING)), 19 | MASK_SCRIPT = (1 << int(GodotErrorType::ERROR_TYPE_SCRIPT)), 20 | MASK_SHADER = (1 << int(GodotErrorType::ERROR_TYPE_SHADER)), 21 | MASK_ALL = (int(MASK_ERROR) | int(MASK_WARNING) | int(MASK_SCRIPT) | int(MASK_SHADER)), 22 | MASK_ALL_EXCEPT_WARNING = (int(MASK_ERROR) | int(MASK_SCRIPT) | int(MASK_SHADER)), 23 | }; 24 | 25 | // Used for exporting as PropertyInfo. 26 | _FORCE_INLINE_ godot::String GODOT_ERROR_MASK_EXPORT_STRING() { return "Error,Warning,Script,Shader"; } 27 | 28 | _FORCE_INLINE_ Level get_sentry_level_for_godot_error_type(GodotErrorType p_error_type) { return p_error_type == GodotErrorType::ERROR_TYPE_WARNING ? LEVEL_WARNING : LEVEL_ERROR; } 29 | _FORCE_INLINE_ LogLevel get_sentry_log_level_for_godot_error_type(GodotErrorType p_error_type) { return p_error_type == GodotErrorType::ERROR_TYPE_WARNING ? LOG_LEVEL_WARN : LOG_LEVEL_ERROR; } 30 | _FORCE_INLINE_ GodotErrorMask godot_error_type_as_mask(GodotErrorType p_error_type) { return (GodotErrorMask)(1 << int(p_error_type)); } 31 | 32 | } //namespace sentry 33 | 34 | #endif // LOGGER_ERROR_TYPES_H 35 | -------------------------------------------------------------------------------- /src/sentry/sentry_attachment.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_attachment.h" 2 | 3 | #include "sentry/util/simple_bind.h" 4 | 5 | #include 6 | 7 | namespace sentry { 8 | 9 | Ref SentryAttachment::create_with_path(const String &p_path) { 10 | ERR_FAIL_COND_V_MSG(p_path.is_empty(), Ref(), "Sentry: Can't create attachment with an empty file path."); 11 | 12 | Ref attachment = memnew(SentryAttachment); 13 | attachment->path = p_path; 14 | return attachment; 15 | } 16 | 17 | Ref SentryAttachment::create_with_bytes(const PackedByteArray &p_bytes, const String &p_filename) { 18 | ERR_FAIL_COND_V_MSG(p_filename.is_empty(), Ref(), "Sentry: Can't create attachment with an empty filename."); 19 | 20 | Ref attachment = memnew(SentryAttachment); 21 | attachment->bytes = p_bytes; 22 | attachment->filename = p_filename; 23 | return attachment; 24 | } 25 | 26 | void SentryAttachment::_bind_methods() { 27 | ClassDB::bind_static_method("SentryAttachment", D_METHOD("create_with_path", "path"), &SentryAttachment::create_with_path); 28 | ClassDB::bind_static_method("SentryAttachment", D_METHOD("create_with_bytes", "bytes", "filename"), &SentryAttachment::create_with_bytes); 29 | 30 | BIND_PROPERTY(SentryAttachment, PropertyInfo(Variant::PACKED_BYTE_ARRAY, "bytes"), set_bytes, get_bytes); 31 | BIND_PROPERTY(SentryAttachment, PropertyInfo(Variant::STRING, "path"), set_path, get_path); 32 | BIND_PROPERTY(SentryAttachment, PropertyInfo(Variant::STRING, "filename"), set_filename, get_filename); 33 | BIND_PROPERTY(SentryAttachment, PropertyInfo(Variant::STRING, "content_type"), set_content_type, get_content_type); 34 | } 35 | 36 | } // namespace sentry 37 | -------------------------------------------------------------------------------- /src/sentry/sentry_timestamp.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_TIMESTAMP_H 2 | #define SENTRY_TIMESTAMP_H 3 | 4 | #include 5 | 6 | using namespace godot; 7 | 8 | namespace sentry { 9 | 10 | class SentryTimestamp : public RefCounted { 11 | GDCLASS(SentryTimestamp, RefCounted); 12 | 13 | private: 14 | // NOTE: Use int64_t for Godot Variant compatibility. 15 | int64_t microseconds_since_unix_epoch = 0; // 1970-01-01 16 | 17 | protected: 18 | static void _bind_methods(); 19 | 20 | String _to_string() const { return to_rfc3339(); } 21 | 22 | public: 23 | // Parse RFC3339 timestamp (YYYY-MM-DDTHH:MM:SS.sssssssssZ or with ±HH:MM offset). 24 | static Ref parse_rfc3339_cstr(const char *p_formatted_cstring); 25 | static Ref parse_rfc3339(const String &p_formatted_string) { return parse_rfc3339_cstr(p_formatted_string.ascii()); } 26 | 27 | // Create with Unix time (seconds since unix epoch) – useful in GDScript. 28 | static Ref from_unix_time(double p_unix_time); 29 | 30 | // Create with microseconds since Unix epoch – lossless. 31 | static Ref from_microseconds_since_unix_epoch(int64_t p_microseconds); 32 | 33 | _FORCE_INLINE_ int64_t get_microseconds_since_unix_epoch() const { return microseconds_since_unix_epoch; } 34 | _FORCE_INLINE_ void set_microseconds_since_unix_epoch(int64_t p_microseconds) { microseconds_since_unix_epoch = p_microseconds; } 35 | 36 | _FORCE_INLINE_ bool equals(const Ref &p_other) { 37 | return p_other.is_valid() ? microseconds_since_unix_epoch == p_other->microseconds_since_unix_epoch : false; 38 | } 39 | 40 | // Return RFC3339 formatted string. 41 | String to_rfc3339() const; 42 | }; 43 | 44 | } // namespace sentry 45 | 46 | #endif // SENTRY_TIMESTAMP_H 47 | -------------------------------------------------------------------------------- /project/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Sentry demo project" 14 | config/version="1.2.0" 15 | run/main_scene="uid://cqiowj0jydds1" 16 | run/main_loop_type="ProjectMainLoop" 17 | config/features=PackedStringArray("4.5") 18 | run/flush_stdout_on_print=true 19 | config/icon="uid://djdyhgbcdue6n" 20 | 21 | [debug] 22 | 23 | file_logging/enable_file_logging=true 24 | settings/gdscript/always_track_call_stacks=true 25 | settings/gdscript/always_track_local_variables=true 26 | 27 | [display] 28 | 29 | window/size/viewport_width=1400 30 | window/size/viewport_height=1080 31 | window/stretch/mode="canvas_items" 32 | window/stretch/aspect="expand" 33 | window/handheld/orientation=1 34 | 35 | [editor_plugins] 36 | 37 | enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg") 38 | 39 | [gdunit4] 40 | 41 | ui/toolbar/run_overall=true 42 | hooks/session_hooks=Dictionary[String, bool]({ 43 | "res://addons/gdUnit4/src/core/hooks/GdUnitHtmlReporterTestSessionHook.gd": false, 44 | "res://addons/gdUnit4/src/core/hooks/GdUnitXMLReporterTestSessionHook.gd": false 45 | }) 46 | 47 | [rendering] 48 | 49 | renderer/rendering_method="gl_compatibility" 50 | renderer/rendering_method.mobile="gl_compatibility" 51 | textures/vram_compression/import_etc2_astc=true 52 | 53 | [sentry] 54 | 55 | options/auto_init=false 56 | options/dsn="https://3f1e095cf2e14598a0bd5b4ff324f712@o447951.ingest.us.sentry.io/6680910" 57 | options/attach_scene_tree=true 58 | logger/include_variables=true 59 | experimental/attach_screenshot=true 60 | experimental/enable_logs=true 61 | -------------------------------------------------------------------------------- /project/test/isolated/test_limit_throttling.gd: -------------------------------------------------------------------------------- 1 | extends GdUnitTestSuite 2 | ## Test error logger throttling limits. 3 | 4 | 5 | signal callback_processed 6 | 7 | var _num_events: int = 0 8 | 9 | 10 | func before() -> void: 11 | SentrySDK.init(func(options: SentryOptions) -> void: 12 | # Allow only two errors to be logged as events within 1 second time window. 13 | options.logger_limits.throttle_events = 2 14 | options.logger_limits.throttle_window_ms = 1000 15 | # Make sure other limits are not interfering. 16 | options.logger_limits.events_per_frame = 88 17 | options.logger_limits.repeated_error_window_ms = 0 18 | options.before_send = _before_send 19 | ) 20 | 21 | 22 | func _before_send(ev: SentryEvent) -> SentryEvent: 23 | if ev.is_crash(): 24 | # Likely processing previous crash. 25 | return ev 26 | _num_events += 1 27 | callback_processed.emit() 28 | return null 29 | 30 | 31 | ## Only two errors should be logged within the assigned time window. 32 | func test_throttling_limits() -> void: 33 | # Wait for special startup limits to expire. 34 | while Engine.get_process_frames() < 10: 35 | await get_tree().process_frame 36 | 37 | monitor_signals(self, false) 38 | 39 | push_error("dummy-error 1") 40 | push_error("dummy-error 2") 41 | push_error("dummy-error 3") 42 | 43 | await assert_signal(self).is_emitted("callback_processed") 44 | await get_tree().process_frame # allow all events to process 45 | assert_int(_num_events).is_equal(2) 46 | 47 | # Wait for throttling window to expire. 48 | await get_tree().create_timer(1.1).timeout 49 | 50 | push_error("dummy-error 4") 51 | push_error("dummy-error 5") 52 | push_error("dummy-error 6") 53 | await assert_signal(self).is_emitted("callback_processed") 54 | await get_tree().process_frame # allow all events to process 55 | assert_int(_num_events).is_equal(4) 56 | -------------------------------------------------------------------------------- /src/sentry/disabled/disabled_sdk.h: -------------------------------------------------------------------------------- 1 | #ifndef DISABLED_SDK_H 2 | #define DISABLED_SDK_H 3 | 4 | #include "disabled_breadcrumb.h" 5 | #include "disabled_event.h" 6 | #include "sentry/internal_sdk.h" 7 | 8 | namespace sentry { 9 | 10 | // Internal SDK that does nothing. 11 | class DisabledSDK : public InternalSDK { 12 | virtual void set_context(const String &p_key, const Dictionary &p_value) override {} 13 | virtual void remove_context(const String &p_key) override {} 14 | 15 | virtual void set_tag(const String &p_key, const String &p_value) override {} 16 | virtual void remove_tag(const String &p_key) override {} 17 | 18 | virtual void set_user(const Ref &p_user) override {} 19 | virtual void remove_user() override {} 20 | 21 | virtual Ref create_breadcrumb() override { return memnew(DisabledBreadcrumb); } 22 | virtual void add_breadcrumb(const Ref &p_breadcrumb) override {} 23 | 24 | virtual void log(LogLevel p_level, const String &p_body, const Dictionary &p_attributes = Dictionary()) override {} 25 | 26 | virtual String capture_message(const String &p_message, Level p_level = sentry::LEVEL_INFO) override { return ""; } 27 | virtual String get_last_event_id() override { return ""; } 28 | 29 | virtual Ref create_event() override { return memnew(DisabledEvent); } 30 | virtual String capture_event(const Ref &p_event) override { return ""; } 31 | 32 | virtual void capture_feedback(const Ref &p_feedback) override {} 33 | 34 | virtual void add_attachment(const Ref &p_attachment) override {} 35 | 36 | virtual void init(const PackedStringArray &p_global_attachments, const Callable &p_configuration_callback) override {} 37 | virtual void close() override {} 38 | virtual bool is_enabled() const override { return false; } 39 | }; 40 | 41 | } // namespace sentry 42 | 43 | #endif // DISABLED_SDK_H 44 | -------------------------------------------------------------------------------- /.github/workflows/unit_tests.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Unit tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | unit-tests: 11 | name: Test ${{matrix.runner}} ${{matrix.arch}} 12 | runs-on: ${{matrix.runner}} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - runner: windows-latest 18 | arch: x86_64 19 | - runner: windows-latest 20 | arch: x86_32 21 | - runner: ubuntu-latest 22 | arch: x86_64 23 | - runner: ubuntu-latest 24 | arch: x86_32 25 | - runner: macos-15 26 | arch: universal 27 | steps: 28 | - name: Checkout repo 29 | uses: actions/checkout@v4 30 | with: 31 | submodules: false # don't initialize submodules automatically 32 | 33 | - name: Prepare testing 34 | uses: ./.github/actions/prepare-testing 35 | with: 36 | arch: ${{ matrix.arch }} 37 | 38 | - name: Run tests 39 | shell: bash 40 | timeout-minutes: 5 41 | run: | 42 | # Exit status codes: 0 - success, 100 - failures, 101 - warnings, 104 - tests not found, 105 - didn't run. 43 | ${GODOT} --headless --path project/ -- run-tests "res://test/suites/" 44 | 45 | - name: Run isolated tests 46 | if: success() || failure() 47 | shell: pwsh 48 | timeout-minutes: 5 49 | run: ./scripts/run-isolated-tests.ps1 50 | 51 | - name: Run JSONAssert tests 52 | if: success() || failure() 53 | shell: bash 54 | timeout-minutes: 2 55 | run: | 56 | ${GODOT} --headless --debug --path project/ \ 57 | --script "res://addons/gdUnit4/bin/GdUnitCmdTool.gd" \ 58 | --ignoreHeadlessMode \ 59 | --continue \ 60 | --add test/util/json_assert/ 61 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_sdk.h: -------------------------------------------------------------------------------- 1 | #ifndef COCOA_SDK_H 2 | #define COCOA_SDK_H 3 | 4 | #include "sentry/internal_sdk.h" 5 | 6 | #include 7 | 8 | using namespace godot; 9 | 10 | namespace sentry::cocoa { 11 | 12 | // Internal SDK utilizing Sentry Cocoa. 13 | class CocoaSDK : public InternalSDK { 14 | private: 15 | String last_event_id; 16 | Ref last_event_id_mutex; 17 | 18 | public: 19 | virtual void set_context(const String &p_key, const Dictionary &p_value) override; 20 | virtual void remove_context(const String &p_key) override; 21 | 22 | virtual void set_tag(const String &p_key, const String &p_value) override; 23 | virtual void remove_tag(const String &p_key) override; 24 | 25 | virtual void set_user(const Ref &p_user) override; 26 | virtual void remove_user() override; 27 | 28 | virtual Ref create_breadcrumb() override; 29 | virtual void add_breadcrumb(const Ref &p_breadcrumb) override; 30 | 31 | virtual void log(LogLevel p_level, const String &p_body, const Dictionary &p_attributes = Dictionary()) override; 32 | 33 | virtual String capture_message(const String &p_message, Level p_level = sentry::LEVEL_INFO) override; 34 | virtual String get_last_event_id() override; 35 | 36 | virtual Ref create_event() override; 37 | virtual String capture_event(const Ref &p_event) override; 38 | 39 | virtual void capture_feedback(const Ref &p_feedback) override; 40 | 41 | virtual void add_attachment(const Ref &p_attachment) override; 42 | 43 | virtual void init(const PackedStringArray &p_global_attachments, const Callable &p_configuration_callback) override; 44 | virtual void close() override; 45 | virtual bool is_enabled() const override; 46 | 47 | CocoaSDK(); 48 | virtual ~CocoaSDK() override; 49 | }; 50 | 51 | } //namespace sentry::cocoa 52 | 53 | #endif // COCOA_SDK_H 54 | -------------------------------------------------------------------------------- /src/sentry/native/native_sdk.h: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_SDK_H 2 | #define NATIVE_SDK_H 3 | 4 | #include "sentry/internal_sdk.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace sentry::native { 10 | 11 | // Internal SDK utilizing sentry-native. 12 | class NativeSDK : public InternalSDK { 13 | private: 14 | sentry_uuid_t last_uuid; 15 | Ref last_uuid_mutex; 16 | bool initialized = false; 17 | 18 | public: 19 | virtual void set_context(const String &p_key, const Dictionary &p_value) override; 20 | virtual void remove_context(const String &p_key) override; 21 | 22 | virtual void set_tag(const String &p_key, const String &p_value) override; 23 | virtual void remove_tag(const String &p_key) override; 24 | 25 | virtual void set_user(const Ref &p_user) override; 26 | virtual void remove_user() override; 27 | 28 | virtual Ref create_breadcrumb() override; 29 | virtual void add_breadcrumb(const Ref &p_breadcrumb) override; 30 | 31 | virtual void log(LogLevel p_level, const String &p_body, const Dictionary &p_attributes = Dictionary()) override; 32 | 33 | virtual String capture_message(const String &p_message, Level p_level = sentry::LEVEL_INFO) override; 34 | virtual String get_last_event_id() override; 35 | 36 | virtual Ref create_event() override; 37 | virtual String capture_event(const Ref &p_event) override; 38 | 39 | virtual void capture_feedback(const Ref &p_feedback) override; 40 | 41 | virtual void add_attachment(const Ref &p_attachment) override; 42 | 43 | virtual void init(const PackedStringArray &p_global_attachments, const Callable &p_configuration_callback) override; 44 | virtual void close() override; 45 | virtual bool is_enabled() const override; 46 | 47 | NativeSDK(); 48 | virtual ~NativeSDK() override; 49 | }; 50 | 51 | } //namespace sentry::native 52 | 53 | #endif // NATIVE_SDK_H 54 | -------------------------------------------------------------------------------- /src/sentry/internal_sdk.h: -------------------------------------------------------------------------------- 1 | #ifndef INTERNAL_SDK_H 2 | #define INTERNAL_SDK_H 3 | 4 | #include "sentry/level.h" 5 | #include "sentry/log_level.h" 6 | #include "sentry/sentry_attachment.h" 7 | #include "sentry/sentry_breadcrumb.h" 8 | #include "sentry/sentry_event.h" 9 | #include "sentry/sentry_feedback.h" 10 | #include "sentry/sentry_user.h" 11 | 12 | #include 13 | #include 14 | 15 | using namespace godot; 16 | 17 | namespace sentry { 18 | 19 | // Interface for SDKs used internally. 20 | class InternalSDK { 21 | public: 22 | virtual void set_context(const String &p_key, const Dictionary &p_value) = 0; 23 | virtual void remove_context(const String &p_key) = 0; 24 | 25 | virtual void set_tag(const String &p_key, const String &p_value) = 0; 26 | virtual void remove_tag(const String &p_key) = 0; 27 | 28 | virtual void set_user(const Ref &p_user) = 0; 29 | virtual void remove_user() = 0; 30 | 31 | virtual Ref create_breadcrumb() = 0; 32 | virtual void add_breadcrumb(const Ref &p_breadcrumb) = 0; 33 | 34 | virtual void log(LogLevel p_level, const String &p_body, const Dictionary &p_attributes = Dictionary()) = 0; 35 | 36 | virtual String capture_message(const String &p_message, Level p_level) = 0; 37 | virtual String get_last_event_id() = 0; 38 | 39 | virtual Ref create_event() = 0; 40 | virtual String capture_event(const Ref &p_event) = 0; 41 | 42 | virtual void capture_feedback(const Ref &p_feedback) = 0; 43 | 44 | virtual void add_attachment(const Ref &p_attachment) = 0; 45 | 46 | virtual void init(const PackedStringArray &p_global_attachments, const Callable &p_configuration_callback) = 0; 47 | virtual void close() = 0; 48 | virtual bool is_enabled() const = 0; 49 | 50 | virtual ~InternalSDK() = default; 51 | }; 52 | 53 | } //namespace sentry 54 | 55 | #endif // INTERNAL_SDK_H 56 | -------------------------------------------------------------------------------- /scripts/run-isolated-tests.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | # Run tests that require isolation. 4 | # Such tests are located in the "project/test/isolated" directory. 5 | 6 | function Highlight { 7 | param ( 8 | [string]$Message 9 | ) 10 | Write-Host $Message -ForegroundColor Cyan 11 | } 12 | 13 | $godot = $env:GODOT 14 | 15 | if (-not $godot) { 16 | Write-Host "GODOT environment variable is not set. Defaulting to `"godot`"." 17 | $godot = "godot" 18 | } 19 | 20 | if (-not (Get-Command $godot -ErrorAction SilentlyContinue)) { 21 | Write-Error "Godot executable not found. Please set the GODOT environment variable." -CategoryActivity "ERROR" 22 | exit 1 23 | } 24 | 25 | $startDir = Get-Location 26 | $scriptDir = Split-Path -Parent (Resolve-Path $MyInvocation.MyCommand.Path) 27 | Set-Location "$scriptDir/../project" 28 | 29 | $exitCode = 0 30 | $numFailed = 0 31 | $numPassed = 0 32 | 33 | Get-ChildItem -Path "test/isolated" -Filter "test_*.gd" | ForEach-Object { 34 | $file = $_.FullName 35 | Highlight "Running isolated test: $file" 36 | 37 | $args = "--headless --path . -s `"res://addons/gdUnit4/bin/GdUnitCmdTool.gd`" --ignoreHeadlessMode -c -a `"$file`"" 38 | $process = Start-Process $godot -ArgumentList $args -PassThru -Wait -NoNewWindow 39 | $err = $process.ExitCode 40 | 41 | Highlight "Finished with exit code: $err" 42 | 43 | if ($err -ne 0) { 44 | $exitCode = $err 45 | $numFailed++ 46 | } else { 47 | $numPassed++ 48 | } 49 | } 50 | 51 | Set-Location $startDir 52 | 53 | Write-Host "--------------------------------------------------------------------------------" 54 | Highlight "Tests finished." 55 | Write-Host "Summary: $numPassed passed, $numFailed failed." 56 | if ($exitCode -eq 0) { 57 | Write-Host "SUCCESS: All isolated tests suites passed." -ForegroundColor Green 58 | } else { 59 | Write-Warning "Some isolated test suites failed!" 60 | } 61 | 62 | exit $exitCode 63 | -------------------------------------------------------------------------------- /src/sentry/sentry_attachment.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_ATTACHMENT_H 2 | #define SENTRY_ATTACHMENT_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef SDK_NATIVE 9 | #include 10 | #endif 11 | 12 | using namespace godot; 13 | 14 | namespace sentry { 15 | 16 | // Represents attachments in the public API. 17 | class SentryAttachment : public RefCounted { 18 | GDCLASS(SentryAttachment, RefCounted); 19 | 20 | private: 21 | PackedByteArray bytes; 22 | String path; 23 | String filename; 24 | String content_type; 25 | 26 | #ifdef SDK_NATIVE 27 | sentry_attachment_t *native_attachment = nullptr; 28 | #endif 29 | 30 | protected: 31 | static void _bind_methods(); 32 | 33 | public: 34 | static Ref create_with_path(const String &p_path); 35 | static Ref create_with_bytes(const PackedByteArray &p_bytes, const String &p_filename); 36 | 37 | PackedByteArray get_bytes() const { return bytes; } 38 | void set_bytes(const PackedByteArray &p_bytes) { bytes = p_bytes; } 39 | 40 | String get_path() const { return path; } 41 | void set_path(const String &p_path) { path = p_path; } 42 | 43 | String get_filename() const { return filename; } 44 | void set_filename(const String &p_filename) { filename = p_filename; } 45 | 46 | String get_content_type() const { return content_type; } 47 | void set_content_type(const String &p_content_type) { content_type = p_content_type; } 48 | 49 | String get_content_type_or_default() const { return content_type.is_empty() ? "application/octet-stream" : content_type; } 50 | 51 | #ifdef SDK_NATIVE 52 | sentry_attachment_t *get_native_attachment() const { return native_attachment; } 53 | void set_native_attachment(sentry_attachment_t *p_native_attachment) { native_attachment = p_native_attachment; } 54 | #endif 55 | 56 | ~SentryAttachment() = default; 57 | }; 58 | 59 | } // namespace sentry 60 | 61 | #endif // SENTRY_ATTACHMENT_H 62 | -------------------------------------------------------------------------------- /.github/workflows/test_android.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Android tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test-android: 11 | name: Test Android 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v4 16 | with: 17 | submodules: false # don't initialize submodules automatically 18 | 19 | - name: Prepare testing 20 | uses: ./.github/actions/prepare-testing 21 | timeout-minutes: 15 22 | with: 23 | arch: x86_64 24 | android: true 25 | 26 | # Needed for Android emulator 27 | - name: Enable KVM 28 | run: | 29 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 30 | sudo udevadm control --reload-rules 31 | sudo udevadm trigger --name-match=kvm 32 | 33 | # Make sure that the required directories and .cfg do exists. Workaround to keep ADV happy on `ubuntu-latest`. 34 | - name: Setup Android directories 35 | run: | 36 | mkdir -p $HOME/.android 37 | mkdir -p $HOME/.android/avd 38 | touch $HOME/.android/repositories.cfg 39 | 40 | - name: Run Android Tests 41 | uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # pin@v2.35.0 42 | timeout-minutes: 30 43 | with: 44 | api-level: 34 45 | target: "google_apis" 46 | channel: "stable" 47 | force-avd-creation: true 48 | disable-animations: true 49 | disable-spellchecker: true 50 | emulator-options: > 51 | -no-window 52 | -no-snapshot-save 53 | -gpu swiftshader_indirect 54 | -noaudio 55 | -no-boot-anim 56 | -camera-back none 57 | -camera-front none 58 | arch: x86_64 59 | script: ./scripts/run-android-tests.sh 60 | -------------------------------------------------------------------------------- /doc_classes/SentryExperimental.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Experimental options for Sentry SDK. 5 | 6 | 7 | Contains configuration options for experimental features of the [SentrySDK]. These features may be unstable or subject to change in future versions. 8 | Access this configuration through [member SentryOptions.experimental]. 9 | 10 | 11 | 12 | 13 | 14 | If assigned, this callback will be called before sending a log message to Sentry. It can be used to modify the log message or prevent it from being sent. 15 | [codeblock] 16 | func _before_send_log(log_entry: SentryLog) -> SentryLog: 17 | # Filter junk. 18 | if log_entry.body == "Junk message": 19 | return null 20 | # Remove sensitive information from log messages. 21 | log_entry.body = log_entry.body.replace("Bruno", "REDACTED") 22 | # Add custom attributes. 23 | log_entry.set_attribute("current_scene", current_scene.name) 24 | return log_entry 25 | [/codeblock] 26 | 27 | 28 | Enables Sentry structured logging functionality. When enabled, Godot's log messages are automatically captured and sent to Sentry, and you gain access to the dedicated logging APIs available through [member SentrySDK.logger]. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /doc_classes/SentryFeedback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Represents user feedback in Sentry. 5 | 6 | 7 | [SentryFeedback] allows you to collect and send user feedback to Sentry. Create an instance of this class, set the required [member message] and optional fields, then submit it using [method SentrySDK.capture_feedback]. 8 | To learn more, visit [url=https://docs.sentry.io/platforms/godot/user-feedback/]User Feedback[/url] documentation. 9 | 10 | 11 | 12 | 13 | 14 | The identifier of an error event in the same project. [i]Optional[/i]. 15 | Use this to explicitly link a related error in the feedback UI. 16 | 17 | 18 | The email of the user who submitted the feedback. [i]Optional[/i]. 19 | If excluded, Sentry attempts to fill this in with user context. Anonymous feedbacks (no name or email) are still accepted. 20 | 21 | 22 | Comments of the user, describing what happened and/or sharing feedback. [i]Required[/i]. 23 | The max length is 4096 characters. 24 | 25 | 26 | The name of the user who submitted the feedback. [i]Optional[/i]. 27 | If excluded, Sentry attempts to fill this in with user context. Anonymous feedbacks (no name or email) are still accepted. 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /doc_classes/SentryLoggerLimits.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Specifies throttling limits for the error logger. 5 | 6 | 7 | These limits govern the behavior of throttling and are used to prevent the SDK from sending too many non-critical and repeating error events. See also [SentryOptions]. 8 | 9 | 10 | 11 | 12 | 13 | Specifies the maximum number of error events to send per processed frame. If exceeded, no further errors will be captured until the next frame. 14 | This serves as a safety measure to prevent the SDK from overloading a single frame. 15 | 16 | 17 | Specifies the minimum time interval in milliseconds between two identical errors. If exceeded, no further errors from the same line of code with the identical message will be captured until the next interval. Set to [code]0[/code] to disable this limit. 18 | 19 | 20 | Specifies the maximum number of events allowed within a sliding time window of [member throttle_window_ms] milliseconds. If exceeded, errors will be captured as breadcrumbs only until capacity is freed. 21 | 22 | 23 | Specifies the time window in milliseconds for [member throttle_events]. Set to [code]0[/code] to disable this limit. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /integration_tests/Utils.ps1: -------------------------------------------------------------------------------- 1 | # Helper functions dot-sourced in integration testing 2 | 3 | 4 | function script:Write-GitHub { 5 | param ( 6 | [Parameter(Mandatory=$true)] 7 | [string]$message 8 | ) 9 | if ($env:GITHUB_ACTIONS) { 10 | Write-Host "${message}" 11 | } 12 | } 13 | 14 | 15 | function script:ConvertTo-AndroidExtras { 16 | param ( 17 | [Parameter(Mandatory=$true)] 18 | [string]$Arguments 19 | ) 20 | 21 | if ([string]::IsNullOrWhiteSpace($Arguments)) { 22 | return "" 23 | } 24 | 25 | # Split arguments into tokens, respecting quoted strings 26 | $tokens = @() 27 | $current = "" 28 | $inQuotes = $false 29 | $quoteChar = $null 30 | $escapeNext = $false 31 | 32 | for ($i = 0; $i -lt $Arguments.Length; $i++) { 33 | $char = $Arguments[$i] 34 | 35 | if ($escapeNext) { 36 | $current += $char 37 | $escapeNext = $false 38 | } 39 | elseif ($char -eq '\') { 40 | $escapeNext = $true 41 | } 42 | elseif (($char -eq '"' -or $char -eq "'") -and -not $inQuotes) { 43 | $inQuotes = $true 44 | $quoteChar = $char 45 | } 46 | elseif ($char -eq $quoteChar -and $inQuotes) { 47 | $inQuotes = $false 48 | $quoteChar = $null 49 | } 50 | elseif ($char -eq ' ' -and -not $inQuotes) { 51 | if ($current.Length -gt 0) { 52 | $tokens += $current 53 | $current = "" 54 | } 55 | } 56 | else { 57 | $current += $char 58 | } 59 | } 60 | 61 | # Add the last token if it exists 62 | if ($current.Length -gt 0) { 63 | $tokens += $current 64 | } 65 | 66 | # Convert tokens to Android intent extras format 67 | $extras = "" 68 | for ($i = 0; $i -lt $tokens.Count; $i++) { 69 | $escapedToken = $tokens[$i] -replace '"', '\"' 70 | $extras += " --es arg$i `"$escapedToken`"" 71 | } 72 | 73 | return $extras.TrimStart() 74 | } 75 | -------------------------------------------------------------------------------- /src/sentry/processing/view_hierarchy_processor.cpp: -------------------------------------------------------------------------------- 1 | #include "view_hierarchy_processor.h" 2 | 3 | #include "sentry/common_defs.h" 4 | #include "sentry/logging/print.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace sentry { 12 | 13 | Ref ViewHierarchyProcessor::process_event(const Ref &p_event) { 14 | #ifdef DEBUG_ENABLED 15 | auto start = std::chrono::high_resolution_clock::now(); 16 | #endif 17 | 18 | std::remove(json_file_path.ptr()); 19 | 20 | if (OS::get_singleton()->get_thread_caller_id() != OS::get_singleton()->get_main_thread_id()) { 21 | sentry::logging::print_debug("Skipping scene tree capture - can only be performed on the main thread"); 22 | return p_event; 23 | } 24 | 25 | sentry::util::UTF8Buffer json_buffer = view_hierarchy_builder.build_json(); 26 | 27 | FILE *f = std::fopen(json_file_path.ptr(), "wb"); 28 | if (f) { 29 | size_t written = std::fwrite(json_buffer.ptr(), 1, json_buffer.get_size(), f); 30 | if (written != json_buffer.get_size()) { 31 | sentry::logging::print_error(vformat("Failed to write scene tree data - only wrote %d bytes out of %d", (int64_t)written, (int64_t)json_buffer.get_size())); 32 | } 33 | std::fclose(f); 34 | } else { 35 | sentry::logging::print_error(vformat("Failed to write scene tree data - unable to open file for writing: %s", json_file_path.get_data())); 36 | } 37 | 38 | #ifdef DEBUG_ENABLED 39 | auto end = std::chrono::high_resolution_clock::now(); 40 | auto duration = std::chrono::duration_cast(end - start); 41 | sentry::logging::print_debug("Capturing scene tree data took ", (int64_t)duration.count(), " usec"); 42 | #endif 43 | 44 | return p_event; 45 | } 46 | 47 | ViewHierarchyProcessor::ViewHierarchyProcessor() { 48 | String path = "user://" SENTRY_VIEW_HIERARCHY_FN; 49 | ERR_FAIL_NULL(ProjectSettings::get_singleton()); 50 | json_file_path = String(ProjectSettings::get_singleton()->globalize_path(path)).utf8(); 51 | } 52 | 53 | } // namespace sentry 54 | -------------------------------------------------------------------------------- /android_lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "io.sentry.godotplugin" 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | minSdk = 24 12 | 13 | consumerProguardFiles("consumer-rules.pro") 14 | 15 | setProperty("archivesBaseName", "sentry_android_godot_plugin") 16 | } 17 | 18 | buildTypes { 19 | release { 20 | isMinifyEnabled = false 21 | proguardFiles( 22 | getDefaultProguardFile("proguard-android-optimize.txt"), 23 | "proguard-rules.pro" 24 | ) 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility = JavaVersion.VERSION_17 29 | targetCompatibility = JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = "17" 33 | } 34 | } 35 | 36 | dependencies { 37 | implementation("org.godotengine:godot:4.4.0.stable") 38 | testImplementation("junit:junit:4.13.2") 39 | 40 | // NOTE: All dependencies below must be also updated in sentry_editor_export_plugin.cpp. 41 | implementation("io.sentry:sentry-android:8.29.0") 42 | } 43 | 44 | val copyDebugAarToProject by tasks.registering(Copy::class) { 45 | description = "Copies generated debug AAR to project" 46 | from("build/outputs/aar") 47 | include("sentry_android_godot_plugin-debug.aar") 48 | into("../project/addons/sentry/bin/android/") 49 | rename("sentry_android_godot_plugin-debug.aar", "sentry_android_godot_plugin.debug.aar") 50 | } 51 | 52 | val copyReleaseAarToProject by tasks.registering(Copy::class) { 53 | description = "Copies generated release AAR to project" 54 | from("build/outputs/aar") 55 | include("sentry_android_godot_plugin-release.aar") 56 | into("../project/addons/sentry/bin/android/") 57 | rename("sentry_android_godot_plugin-release.aar", "sentry_android_godot_plugin.release.aar") 58 | } 59 | 60 | tasks.named("assemble").configure { 61 | finalizedBy(copyDebugAarToProject) 62 | finalizedBy(copyReleaseAarToProject) 63 | } 64 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_breadcrumb.mm: -------------------------------------------------------------------------------- 1 | #include "cocoa_breadcrumb.h" 2 | 3 | #include "sentry/cocoa/cocoa_includes.h" 4 | #include "sentry/cocoa/cocoa_util.h" 5 | 6 | namespace sentry::cocoa { 7 | 8 | void CocoaBreadcrumb::set_message(const String &p_message) { 9 | cocoa_breadcrumb.message = string_to_objc_or_nil_if_empty(p_message); 10 | } 11 | 12 | String CocoaBreadcrumb::get_message() const { 13 | return string_from_objc(cocoa_breadcrumb.message); 14 | } 15 | 16 | void CocoaBreadcrumb::set_category(const String &p_category) { 17 | cocoa_breadcrumb.category = string_to_objc_or_nil_if_empty(p_category); 18 | } 19 | 20 | String CocoaBreadcrumb::get_category() const { 21 | return string_from_objc(cocoa_breadcrumb.category); 22 | } 23 | 24 | void CocoaBreadcrumb::set_level(sentry::Level p_level) { 25 | cocoa_breadcrumb.level = sentry_level_to_objc(p_level); 26 | } 27 | 28 | sentry::Level CocoaBreadcrumb::get_level() const { 29 | return sentry_level_from_objc(cocoa_breadcrumb.level); 30 | } 31 | 32 | void CocoaBreadcrumb::set_type(const String &p_type) { 33 | cocoa_breadcrumb.type = string_to_objc_or_nil_if_empty(p_type); 34 | } 35 | 36 | String CocoaBreadcrumb::get_type() const { 37 | return string_from_objc(cocoa_breadcrumb.type); 38 | } 39 | 40 | void CocoaBreadcrumb::set_data(const Dictionary &p_data) { 41 | cocoa_breadcrumb.data = dictionary_to_objc(p_data); 42 | } 43 | 44 | Ref CocoaBreadcrumb::get_timestamp() { 45 | if (cocoa_breadcrumb.timestamp == nil) { 46 | return Ref(); 47 | } 48 | 49 | NSTimeInterval seconds = [cocoa_breadcrumb.timestamp timeIntervalSince1970]; 50 | return SentryTimestamp::from_unix_time(seconds); 51 | } 52 | 53 | CocoaBreadcrumb::CocoaBreadcrumb() : 54 | cocoa_breadcrumb([[objc::SentryBreadcrumb alloc] init]) { 55 | } 56 | 57 | CocoaBreadcrumb::CocoaBreadcrumb(objc::SentryBreadcrumb *p_cocoa_breadcrumb) : 58 | cocoa_breadcrumb(p_cocoa_breadcrumb) { 59 | if (!p_cocoa_breadcrumb) { 60 | cocoa_breadcrumb = [[objc::SentryBreadcrumb alloc] init]; 61 | ERR_PRINT_ONCE("Sentry: Internal error - cocoa breadcrumb instance is null"); 62 | } 63 | } 64 | 65 | } //namespace sentry::cocoa 66 | -------------------------------------------------------------------------------- /project/project_main_loop.gd: -------------------------------------------------------------------------------- 1 | class_name ProjectMainLoop 2 | extends SceneTree 3 | ## Example of initializing and configuring Sentry from code. 4 | ## 5 | ## The earliest place to initialize Sentry in script is in the MainLoop._initialize(). 6 | ## Tip: You can assign "ProjectMainLoop" as your main loop class in the project settings 7 | ## under `application/run/main_loop_type`. 8 | 9 | 10 | signal before_send_log(log_entry) 11 | 12 | 13 | func _initialize() -> void: 14 | if _is_running_tests_from_editor() or _is_running_cli_command(): 15 | # Not a normal start -- don't initialize Sentry. 16 | return 17 | 18 | SentrySDK.init(func(options: SentryOptions) -> void: 19 | print("INFO: [ProjectMainLoop] Initializing SDK from GDScript") 20 | 21 | options.debug = true 22 | options.release = "sentry-godot-demo@" + ProjectSettings.get_setting("application/config/version") 23 | options.environment = "demo" 24 | 25 | # Set up event callbacks 26 | options.before_send = _on_before_send_to_sentry 27 | options.before_send_log = _on_before_send_log_to_sentry 28 | ) 29 | 30 | # Post-initialize 31 | # SentrySDK.add_attachment(...) 32 | # ... 33 | 34 | 35 | ## before_send example 36 | func _on_before_send_to_sentry(ev: SentryEvent) -> SentryEvent: 37 | print("INFO: [ProjectMainLoop] Processing event: ", ev.id) 38 | var error_message: String = ev.get_exception_value(0) 39 | if error_message.contains("Bruno"): 40 | print("INFO: [ProjectMainLoop] Removing sensitive information from the event") 41 | var redacted_message := error_message.replace("Bruno", "REDACTED") 42 | ev.set_exception_value(0, redacted_message) 43 | elif error_message == "junk": 44 | print("INFO: [ProjectMainLoop] Discarding event with error message 'junk'") 45 | return null 46 | return ev 47 | 48 | 49 | ## before_send_log 50 | func _on_before_send_log_to_sentry(entry: SentryLog) -> SentryLog: 51 | before_send_log.emit(entry) 52 | return entry 53 | 54 | 55 | func _is_running_tests_from_editor() -> bool: 56 | return "res://addons/gdUnit4/src/core/runners/GdUnitTestRunner.tscn" in OS.get_cmdline_args() 57 | 58 | 59 | func _is_running_cli_command() -> bool: 60 | return CLIParser.should_execute() 61 | -------------------------------------------------------------------------------- /android_lib/src/main/java/io/sentry/godotplugin/UtilityFunctions.kt: -------------------------------------------------------------------------------- 1 | package io.sentry.godotplugin 2 | 3 | import io.sentry.SentryLevel 4 | import io.sentry.SentryLogLevel 5 | import java.util.Date 6 | import java.time.Instant 7 | 8 | fun Int.toSentryLevel(): SentryLevel = 9 | when (this) { 10 | 0 -> SentryLevel.DEBUG 11 | 1 -> SentryLevel.INFO 12 | 2 -> SentryLevel.WARNING 13 | 3 -> SentryLevel.ERROR 14 | 4 -> SentryLevel.FATAL 15 | else -> SentryLevel.ERROR 16 | } 17 | 18 | fun Int.toSentryLogLevel(): SentryLogLevel = 19 | when (this) { 20 | 0 -> SentryLogLevel.TRACE 21 | 1 -> SentryLogLevel.DEBUG 22 | 2 -> SentryLogLevel.INFO 23 | 3 -> SentryLogLevel.WARN 24 | 4 -> SentryLogLevel.ERROR 25 | 5 -> SentryLogLevel.FATAL 26 | else -> SentryLogLevel.INFO 27 | } 28 | 29 | fun SentryLevel.toInt(): Int = 30 | when (this) { 31 | SentryLevel.DEBUG -> 0 32 | SentryLevel.INFO -> 1 33 | SentryLevel.WARNING -> 2 34 | SentryLevel.ERROR -> 3 35 | SentryLevel.FATAL -> 4 36 | } 37 | 38 | fun SentryLogLevel.toInt(): Int = 39 | when (this) { 40 | SentryLogLevel.TRACE -> 0 41 | SentryLogLevel.DEBUG -> 1 42 | SentryLogLevel.INFO -> 2 43 | SentryLogLevel.WARN -> 3 44 | SentryLogLevel.ERROR -> 4 45 | SentryLogLevel.FATAL -> 5 46 | } 47 | 48 | fun Long.microsecondsToTimestamp(): Date { 49 | val millis = this / 1_000 50 | return Date(millis) 51 | } 52 | 53 | fun Date.toMicros(): Long { 54 | val date: Date = this@toMicros 55 | return date.time * 1000 56 | } 57 | 58 | fun Any?.toIntOrThrow(): Int = 59 | when (this) { 60 | is Int -> this 61 | is Long -> this.toInt() 62 | else -> throw IllegalArgumentException("Expected Int or Long, got ${this?.let { it::class } ?: "null"}") 63 | } 64 | 65 | fun Any?.toLongOrNull(): Long? = 66 | when (this) { 67 | is Long -> this 68 | is Int -> this.toLong() 69 | is Short -> this.toLong() 70 | is Byte -> this.toLong() 71 | is Number -> this.toLong() 72 | is String -> this.toLongOrNull() 73 | else -> null 74 | } 75 | -------------------------------------------------------------------------------- /project/test/suites/test_sdk.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Test SentrySDK methods. 3 | 4 | 5 | signal callback_processed 6 | 7 | 8 | ## SentrySDK.capture_message() should return a non-empty event ID, which must match the ID returned by the get_last_event_id() call. 9 | func test_capture_message_id() -> void: 10 | # Ensure events are not discarded 11 | SentrySDK._set_before_send(func(ev): return ev) 12 | 13 | var event_id := SentrySDK.capture_message("capture_message_test", SentrySDK.LEVEL_DEBUG) 14 | 15 | assert_str(event_id).is_not_empty() 16 | assert_str(SentrySDK.get_last_event_id()).is_not_empty() 17 | assert_str(event_id).is_equal(SentrySDK.get_last_event_id()) 18 | 19 | 20 | ## SentrySDK.set_tag() should assign a tag to the event object. 21 | func test_set_tag() -> void: 22 | SentrySDK._set_before_send( 23 | func(ev: SentryEvent): 24 | assert_str(ev.get_tag("custom-tag")).is_equal("custom-tag-value") 25 | assert_str(ev.get_tag("utf8-test")).is_equal("Hello 世界! 👋") 26 | callback_processed.emit() 27 | return null) 28 | 29 | SentrySDK.set_tag("custom-tag", "custom-tag-value") 30 | SentrySDK.set_tag("utf8-test", "Hello 世界! 👋") 31 | 32 | var monitor := monitor_signals(self, false) 33 | SentrySDK.capture_message("test-tags") 34 | await assert_signal(monitor).is_emitted("callback_processed") 35 | 36 | 37 | ## SentrySDK.remove_tag() should remove a tag from the event object. 38 | func test_remove_tag() -> void: 39 | SentrySDK._set_before_send( 40 | func(ev: SentryEvent): 41 | assert_str(ev.get_tag("custom-tag")).is_empty() 42 | callback_processed.emit() 43 | return null) 44 | 45 | SentrySDK.set_tag("custom-tag", "custom-tag-value") 46 | SentrySDK.remove_tag("custom-tag") 47 | 48 | var monitor := monitor_signals(self, false) 49 | SentrySDK.capture_message("test-tags") 50 | await assert_signal(monitor).is_emitted("callback_processed") 51 | 52 | 53 | ## SentrySDK Variant conversion should not cause stack overflow. 54 | func test_variant_conversion_against_stack_overflow() -> void: 55 | var dict: Dictionary = { "some_key": "some_value"} 56 | var arr: Array = [dict, "another_value"] 57 | dict["array"] = arr 58 | SentrySDK.set_context("broken_context", dict) 59 | # Unset context 60 | SentrySDK.set_context("broken_context", {}) 61 | -------------------------------------------------------------------------------- /site_scons/site_tools/plist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tool to generate Info.plist. 3 | """ 4 | 5 | import os 6 | from SCons.Script import Builder, Action 7 | 8 | 9 | def generate_framework_plist(target, source, env): 10 | bundle_executable = env.get("bundle_executable", "MyFramework") 11 | bundle_identifier = env.get("bundle_identifier", "com.example.MyFramework") 12 | bundle_name = env.get("bundle_name", bundle_executable) 13 | bundle_version_string = env.get("bundle_version", "1.0") 14 | bundle_version = bundle_version_string.split("-", 1)[0] 15 | bundle_platforms = env.get("bundle_platforms", ["MacOSX"]) 16 | bundle_package_type = env.get("bundle_package_type", "FMWK") # FMWK or BNDL 17 | bundle_min_system = env.get("bundle_min_system", env.get("macos_deployment_target", "10.13")) 18 | 19 | platforms_content = "\n".join(f" {p}" for p in bundle_platforms) 20 | 21 | content = f""" 22 | 23 | 24 | 25 | CFBundleExecutable 26 | {bundle_executable} 27 | CFBundleIdentifier 28 | {bundle_identifier} 29 | CFBundleInfoDictionaryVersion 30 | 6.0 31 | CFBundleName 32 | {bundle_name} 33 | CFBundlePackageType 34 | {bundle_package_type} 35 | CFBundleShortVersionString 36 | {bundle_version_string} 37 | CFBundleSupportedPlatforms 38 | 39 | {platforms_content} 40 | 41 | CFBundleVersion 42 | {bundle_version} 43 | LSMinimumSystemVersion 44 | {bundle_min_system} 45 | 46 | """ 47 | 48 | with open(str(target[0]), "w") as f: 49 | f.write(content) 50 | 51 | return None 52 | 53 | 54 | def generate(env): 55 | plist_builder = Builder(action=Action(generate_framework_plist, cmdstr="Generating Info.plist for $TARGET")) 56 | env.Append(BUILDERS={"FrameworkPlist": plist_builder}) 57 | 58 | 59 | def exists(env): 60 | return True 61 | -------------------------------------------------------------------------------- /project/views/enrich_events.gd: -------------------------------------------------------------------------------- 1 | extends VBoxContainer 2 | 3 | @onready var breadcrumb_message: LineEdit = %BreadcrumbMessage 4 | @onready var breadcrumb_category: LineEdit = %BreadcrumbCategory 5 | @onready var tag_key: LineEdit = %TagKey 6 | @onready var tag_value: LineEdit = %TagValue 7 | @onready var context_name: LineEdit = %ContextName 8 | @onready var context_expression: CodeEdit = %ContextExpression 9 | 10 | 11 | func _ready() -> void: 12 | pass 13 | 14 | 15 | func _on_add_breadcrumb_button_pressed() -> void: 16 | var crumb := SentryBreadcrumb.create(breadcrumb_message.text) 17 | crumb.category = breadcrumb_category.text 18 | SentrySDK.add_breadcrumb(crumb) 19 | DemoOutput.print_info("Breadcrumb added.") 20 | 21 | 22 | func _on_add_tag_button_pressed() -> void: 23 | SentrySDK.set_tag(tag_key.text, tag_value.text) 24 | if not tag_key.text.is_empty(): 25 | DemoOutput.print_info("Tag added.") 26 | 27 | 28 | func _on_set_context_pressed() -> void: 29 | if context_name.text.is_empty(): 30 | DemoOutput.print_info("Please provide a name for the context.") 31 | return 32 | 33 | # Filter out comments because Expression doesn't support them. 34 | var expr_lines := Array(context_expression.text.split("\n")).filter( 35 | func(s: String): return not s.begins_with("#")) 36 | var filtered_expression := "".join(expr_lines) 37 | 38 | # Parsing expression dictionary. 39 | var expr := Expression.new() 40 | var error: Error = expr.parse(filtered_expression) 41 | if error == OK: 42 | var result = expr.execute() 43 | if typeof(result) == TYPE_DICTIONARY: 44 | # Adding context. 45 | SentrySDK.set_context(context_name.text, result) 46 | DemoOutput.print_info("Context added.") 47 | else: 48 | DemoOutput.print_err("Failed set context: Dictionary is expected, but found: " + type_string(typeof(result))) 49 | else: 50 | DemoOutput.print_err("Failed to parse expression: " + expr.get_error_text()) 51 | 52 | 53 | func _on_attach_button_pressed() -> void: 54 | var content: String = %AttachmentContent.text 55 | var bytes: PackedByteArray = content.to_utf8_buffer() 56 | var attachment := SentryAttachment.create_with_bytes(bytes, "hello.txt") 57 | attachment.content_type = "text/plain" 58 | SentrySDK.add_attachment(attachment) 59 | DemoOutput.print_info("Attachment added.") 60 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ⚙️ CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | - "release/**" 8 | paths-ignore: 9 | - "*.md" 10 | 11 | pull_request: 12 | paths-ignore: 13 | - "*.md" 14 | 15 | # Cancel in-progress runs on PR update and on push. 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 18 | cancel-in-progress: true 19 | 20 | permissions: 21 | contents: read 22 | 23 | jobs: 24 | build-extension: 25 | name: 🔌 Build GDExtension 26 | uses: ./.github/workflows/build_gdextension.yml 27 | 28 | static-checks: 29 | name: 🔎 Static checks 30 | needs: build-extension 31 | uses: ./.github/workflows/static_checks.yml 32 | 33 | unit-tests: 34 | name: 🧪 Unit tests 35 | needs: build-extension 36 | uses: ./.github/workflows/unit_tests.yml 37 | 38 | test-android: 39 | name: 🧪 Android tests 40 | needs: build-extension 41 | uses: ./.github/workflows/test_android.yml 42 | 43 | test-integration: 44 | name: 🧪 Integration tests 45 | needs: build-extension 46 | uses: ./.github/workflows/test_integration.yml 47 | secrets: 48 | SENTRY_API_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} 49 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 50 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 51 | 52 | package: 53 | name: 📦 Package 54 | needs: build-extension 55 | uses: ./.github/workflows/package.yml 56 | secrets: 57 | APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} 58 | APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} 59 | APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} 60 | 61 | # Deleting "sentry-godot-gdextension" artifact due to broken file permissions. 62 | # This encourages using the artifact from the "package" job instead which has correct permissions. 63 | # See issue: https://github.com/getsentry/sentry-godot/issues/41 64 | cleanup: 65 | name: 🗑️ Cleanup 66 | needs: [package, unit-tests, test-android, test-integration, static-checks] 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Delete sentry-godot-gdextension artifact 70 | uses: geekyeggo/delete-artifact@f275313e70c08f6120db482d7a6b98377786765b # v5.1.0 71 | with: 72 | name: sentry-godot-gdextension 73 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_event.h: -------------------------------------------------------------------------------- 1 | #ifndef COCOA_EVENT_H 2 | #define COCOA_EVENT_H 3 | 4 | #include "sentry/cocoa/cocoa_includes.h" 5 | #include "sentry/sentry_event.h" 6 | 7 | namespace sentry::cocoa { 8 | 9 | class CocoaEvent : public sentry::SentryEvent { 10 | GDCLASS(CocoaEvent, sentry::SentryEvent); 11 | 12 | private: 13 | objc::SentryEvent *cocoa_event = nullptr; 14 | 15 | protected: 16 | static void _bind_methods() {} 17 | 18 | public: 19 | _FORCE_INLINE_ objc::SentryEvent *get_cocoa_event() const { return cocoa_event; } 20 | 21 | virtual String get_id() const override; 22 | 23 | virtual void set_message(const String &p_message) override; 24 | virtual String get_message() const override; 25 | 26 | virtual void set_timestamp(const Ref &p_timestamp) override; 27 | virtual Ref get_timestamp() const override; 28 | 29 | virtual String get_platform() const override; 30 | 31 | virtual void set_level(sentry::Level p_level) override; 32 | virtual sentry::Level get_level() const override; 33 | 34 | virtual void set_logger(const String &p_logger) override; 35 | virtual String get_logger() const override; 36 | 37 | virtual void set_release(const String &p_release) override; 38 | virtual String get_release() const override; 39 | 40 | virtual void set_dist(const String &p_dist) override; 41 | virtual String get_dist() const override; 42 | 43 | virtual void set_environment(const String &p_environment) override; 44 | virtual String get_environment() const override; 45 | 46 | virtual void set_tag(const String &p_key, const String &p_value) override; 47 | virtual void remove_tag(const String &p_key) override; 48 | virtual String get_tag(const String &p_key) override; 49 | 50 | virtual void merge_context(const String &p_key, const Dictionary &p_value) override; 51 | 52 | virtual void add_exception(const Exception &p_exception) override; 53 | 54 | virtual int get_exception_count() const override; 55 | virtual void set_exception_value(int p_index, const String &p_value) override; 56 | virtual String get_exception_value(int p_index) const override; 57 | 58 | virtual bool is_crash() const override; 59 | 60 | virtual String to_json() const override; 61 | 62 | CocoaEvent(objc::SentryEvent *p_cocoa_event); 63 | CocoaEvent(); 64 | virtual ~CocoaEvent() override; 65 | }; 66 | 67 | } // namespace sentry::cocoa 68 | 69 | #endif // COCOA_EVENT_H 70 | -------------------------------------------------------------------------------- /src/sentry/native/native_event.h: -------------------------------------------------------------------------------- 1 | #ifndef NATIVE_EVENT_H 2 | #define NATIVE_EVENT_H 3 | 4 | #include "sentry/sentry_event.h" 5 | 6 | #include 7 | 8 | namespace sentry::native { 9 | 10 | // Event class that is used with the NativeSDK. 11 | class NativeEvent : public SentryEvent { 12 | GDCLASS(NativeEvent, SentryEvent); 13 | 14 | private: 15 | sentry_value_t native_event; 16 | bool _is_crash = false; 17 | 18 | protected: 19 | static void _bind_methods() {} 20 | 21 | public: 22 | sentry_value_t get_native_value() const { return native_event; } 23 | 24 | virtual String get_id() const override; 25 | 26 | virtual void set_message(const String &p_message) override; 27 | virtual String get_message() const override; 28 | 29 | virtual void set_timestamp(const Ref &p_timestamp) override; 30 | virtual Ref get_timestamp() const override; 31 | 32 | virtual String get_platform() const override; 33 | 34 | virtual void set_level(sentry::Level p_level) override; 35 | virtual sentry::Level get_level() const override; 36 | 37 | virtual void set_logger(const String &p_logger) override; 38 | virtual String get_logger() const override; 39 | 40 | virtual void set_release(const String &p_release) override; 41 | virtual String get_release() const override; 42 | 43 | virtual void set_dist(const String &p_dist) override; 44 | virtual String get_dist() const override; 45 | 46 | virtual void set_environment(const String &p_environment) override; 47 | virtual String get_environment() const override; 48 | 49 | virtual void set_tag(const String &p_key, const String &p_value) override; 50 | virtual void remove_tag(const String &p_key) override; 51 | virtual String get_tag(const String &p_key) override; 52 | 53 | virtual void merge_context(const String &p_key, const Dictionary &p_value) override; 54 | 55 | virtual void add_exception(const Exception &p_exception) override; 56 | 57 | virtual int get_exception_count() const override; 58 | virtual void set_exception_value(int p_index, const String &p_value) override; 59 | virtual String get_exception_value(int p_index) const override; 60 | 61 | virtual bool is_crash() const override; 62 | 63 | virtual String to_json() const override; 64 | 65 | NativeEvent(sentry_value_t p_event, bool p_is_crash); 66 | NativeEvent(); 67 | virtual ~NativeEvent() override; 68 | }; 69 | 70 | } //namespace sentry::native 71 | 72 | #endif // NATIVE_EVENT_H 73 | -------------------------------------------------------------------------------- /project/mobile.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=7 format=3 uid="uid://dpppyaeqgrcn1"] 2 | 3 | [ext_resource type="Script" uid="uid://cw2874mkwddfr" path="res://mobile.gd" id="1_bj8h8"] 4 | [ext_resource type="PackedScene" uid="uid://bxi26vu5tlqas" path="res://views/capture_events.tscn" id="2_xux57"] 5 | [ext_resource type="PackedScene" uid="uid://dyoaec2d7uung" path="res://views/enrich_events.tscn" id="3_p64qd"] 6 | [ext_resource type="PackedScene" uid="uid://cywnvytpa2bec" path="res://views/tools.tscn" id="4_p64qd"] 7 | [ext_resource type="PackedScene" uid="uid://dllqhtd731wtc" path="res://views/output_pane.tscn" id="4_xux57"] 8 | 9 | [sub_resource type="Theme" id="Theme_bj8h8"] 10 | BoxContainer/constants/separation = 8 11 | 12 | [node name="Mobile" type="CanvasLayer"] 13 | script = ExtResource("1_bj8h8") 14 | 15 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 16 | anchors_preset = 15 17 | anchor_right = 1.0 18 | anchor_bottom = 1.0 19 | grow_horizontal = 2 20 | grow_vertical = 2 21 | size_flags_horizontal = 3 22 | size_flags_vertical = 3 23 | theme = SubResource("Theme_bj8h8") 24 | metadata/_edit_lock_ = true 25 | metadata/_edit_use_anchors_ = true 26 | 27 | [node name="Spacer" type="Control" parent="VBoxContainer"] 28 | custom_minimum_size = Vector2(0, 50) 29 | layout_mode = 2 30 | 31 | [node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer"] 32 | layout_mode = 2 33 | size_flags_horizontal = 3 34 | size_flags_vertical = 3 35 | 36 | [node name="TabContainer" type="TabContainer" parent="VBoxContainer/VBoxContainer"] 37 | layout_mode = 2 38 | size_flags_horizontal = 3 39 | size_flags_vertical = 3 40 | size_flags_stretch_ratio = 1.4 41 | current_tab = 1 42 | tab_focus_mode = 0 43 | 44 | [node name="Enrich Events" parent="VBoxContainer/VBoxContainer/TabContainer" instance=ExtResource("3_p64qd")] 45 | visible = false 46 | layout_mode = 2 47 | metadata/_tab_index = 0 48 | 49 | [node name="Capture Events" parent="VBoxContainer/VBoxContainer/TabContainer" instance=ExtResource("2_xux57")] 50 | layout_mode = 2 51 | metadata/_tab_index = 1 52 | 53 | [node name="Tools" parent="VBoxContainer/VBoxContainer/TabContainer" instance=ExtResource("4_p64qd")] 54 | unique_name_in_owner = true 55 | visible = false 56 | layout_mode = 2 57 | 58 | [node name="OutputPane" parent="VBoxContainer/VBoxContainer" instance=ExtResource("4_xux57")] 59 | layout_mode = 2 60 | 61 | [node name="Spacer2" type="Control" parent="VBoxContainer"] 62 | custom_minimum_size = Vector2(0, 20) 63 | layout_mode = 2 64 | -------------------------------------------------------------------------------- /src/sentry/processing/process_event.cpp: -------------------------------------------------------------------------------- 1 | #include "process_event.h" 2 | 3 | #include "sentry/contexts.h" 4 | #include "sentry/logging/print.h" 5 | #include "sentry/processing/sentry_event_processor.h" 6 | #include "sentry/sentry_options.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace sentry { 16 | 17 | Ref process_event(const Ref &p_event) { 18 | if (p_event.is_null()) { 19 | sentry::logging::print_error("attempted to process a null event"); 20 | return nullptr; 21 | } 22 | 23 | sentry::logging::print_debug("processing event ", p_event->get_id()); 24 | 25 | Ref event = p_event; 26 | 27 | // Inject contexts 28 | HashMap contexts = sentry::contexts::make_event_contexts(); 29 | for (const auto &kv : contexts) { 30 | event->merge_context(kv.key, kv.value); 31 | } 32 | 33 | // Event processors 34 | for (const Ref &processor : SentryOptions::get_singleton()->get_event_processors()) { 35 | event = processor->process_event(event); 36 | if (event.is_null()) { 37 | return event; 38 | } else if (event != p_event) { 39 | sentry::logging::print_error("event processor returned a different event object – discarding processor result"); 40 | event = p_event; // Reset to original event 41 | } 42 | } 43 | 44 | // Before send callback 45 | if (const Callable &before_send = SentryOptions::get_singleton()->get_before_send(); before_send.is_valid()) { 46 | event = before_send.call(event); 47 | 48 | if (event.is_valid() && event != p_event) { 49 | static bool first_print = true; 50 | if (unlikely(first_print)) { 51 | // Note: Only push error once to avoid infinite feedback loop. 52 | ERR_PRINT("Sentry: before_send callback must return the same event object or null."); 53 | first_print = false; 54 | } else { 55 | sentry::logging::print_error("before_send callback must return the same event object or null."); 56 | } 57 | return p_event; 58 | } 59 | 60 | if (event.is_valid()) { 61 | sentry::logging::print_debug("before_send processed ", p_event->get_id()); 62 | } else { 63 | sentry::logging::print_debug("before_send discarded ", p_event->get_id()); 64 | } 65 | } 66 | 67 | return event; 68 | } 69 | 70 | } // namespace sentry 71 | -------------------------------------------------------------------------------- /project/test/suites/test_event_integrity.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Verify that event properties are preserved through the SDK flow. 3 | 4 | 5 | signal callback_processed 6 | 7 | var before_send: Callable # (event: SentryEvent) -> SentryEvent 8 | 9 | var created_id: String 10 | 11 | 12 | func _before_send(event: SentryEvent) -> SentryEvent: 13 | callback_processed.emit.call_deferred() 14 | return before_send.call(event) 15 | 16 | 17 | @warning_ignore("unused_parameter") 18 | func test_event_integrity(timeout := 10000) -> void: 19 | _capture_event() 20 | await assert_signal(self).is_emitted("callback_processed") 21 | 22 | 23 | @warning_ignore("unused_parameter") 24 | func test_threaded_event_capture(timeout := 10000) -> void: 25 | var thread := Thread.new() 26 | thread.start(_capture_event) 27 | await assert_signal(self).is_emitted("callback_processed") 28 | thread.wait_to_finish() 29 | 30 | 31 | func _capture_event() -> void: 32 | SentrySDK._set_before_send(_before_send) 33 | 34 | var event := SentrySDK.create_event() 35 | event.message = "integrity-check" 36 | event.level = SentrySDK.LEVEL_DEBUG 37 | event.logger = "custom-logger" 38 | event.release = "custom-release" 39 | event.dist = "custom-dist" 40 | event.environment = "custom-environment" 41 | event.set_tag("custom-tag", "custom-tag-value") 42 | created_id = event.id 43 | 44 | before_send = func(ev: SentryEvent) -> SentryEvent: 45 | assert_str(ev.message).is_equal("integrity-check") 46 | assert_int(ev.level).is_equal(SentrySDK.LEVEL_DEBUG) 47 | assert_str(ev.logger).is_equal("custom-logger") 48 | assert_str(ev.release).is_equal("custom-release") 49 | assert_str(ev.dist).is_equal("custom-dist") 50 | assert_str(ev.environment).is_equal("custom-environment") 51 | assert_str(ev.get_tag("custom-tag")).is_equal("custom-tag-value") 52 | assert_str(ev.id).is_equal(created_id) 53 | assert_str(ev.platform).is_not_empty() 54 | assert_bool(ev.is_crash()).is_false() 55 | return null 56 | 57 | SentrySDK.capture_event(event) 58 | 59 | 60 | func test_event_exception_interface() -> void: 61 | var error_message := "Testing 123" 62 | 63 | before_send = func(ev: SentryEvent) -> SentryEvent: 64 | assert_int(ev.get_exception_count()).is_greater_equal(1) 65 | assert_str(ev.get_exception_value(0)).is_equal("Testing 123") 66 | ev.set_exception_value(0, "New value") 67 | assert_str(ev.get_exception_value(0)).is_equal("New value") 68 | return null 69 | 70 | push_error(error_message) 71 | await assert_signal(self).is_emitted("callback_processed") 72 | -------------------------------------------------------------------------------- /src/sentry/android/android_util.cpp: -------------------------------------------------------------------------------- 1 | #include "android_util.h" 2 | 3 | #include "sentry/common_defs.h" 4 | 5 | using namespace godot; 6 | 7 | namespace sentry::android { 8 | 9 | Variant sanitize_variant(const Variant &p_value, int p_depth) { 10 | switch (p_value.get_type()) { 11 | case Variant::DICTIONARY: { 12 | if (p_depth > VARIANT_CONVERSION_MAX_DEPTH) { 13 | ERR_PRINT_ONCE("Sentry: Maximum Variant depth reached!"); 14 | return Variant(); 15 | } 16 | 17 | Dictionary old_dict = p_value; 18 | Dictionary new_dict; 19 | 20 | const Array &keys = old_dict.keys(); 21 | for (int i = 0; i < keys.size(); i++) { 22 | const Variant &key = keys[i]; 23 | new_dict[key.stringify()] = sanitize_variant(old_dict[key], p_depth + 1); 24 | } 25 | 26 | return new_dict; 27 | } break; 28 | case Variant::ARRAY: 29 | case Variant::PACKED_BYTE_ARRAY: 30 | case Variant::PACKED_INT32_ARRAY: 31 | case Variant::PACKED_INT64_ARRAY: 32 | case Variant::PACKED_FLOAT32_ARRAY: 33 | case Variant::PACKED_FLOAT64_ARRAY: 34 | case Variant::PACKED_STRING_ARRAY: 35 | case Variant::PACKED_VECTOR2_ARRAY: 36 | case Variant::PACKED_VECTOR3_ARRAY: 37 | case Variant::PACKED_COLOR_ARRAY: 38 | case Variant::PACKED_VECTOR4_ARRAY: { 39 | if (p_depth > VARIANT_CONVERSION_MAX_DEPTH) { 40 | ERR_PRINT_ONCE("Sentry: Maximum Variant depth reached!"); 41 | return Variant(); 42 | } 43 | 44 | Array arr; 45 | bool oob = false; 46 | bool valid = true; 47 | int i = 0; 48 | 49 | do { 50 | Variant item = p_value.get_indexed(i++, valid, oob); 51 | if (valid) { 52 | arr.append(sanitize_variant(item, p_depth + 1)); 53 | } 54 | } while (!oob); 55 | 56 | return arr; 57 | } break; 58 | case Variant::VECTOR2: 59 | case Variant::VECTOR2I: 60 | case Variant::RECT2: 61 | case Variant::RECT2I: 62 | case Variant::VECTOR3: 63 | case Variant::VECTOR3I: 64 | case Variant::TRANSFORM2D: 65 | case Variant::VECTOR4: 66 | case Variant::VECTOR4I: 67 | case Variant::PLANE: 68 | case Variant::QUATERNION: 69 | case Variant::AABB: 70 | case Variant::BASIS: 71 | case Variant::TRANSFORM3D: 72 | case Variant::PROJECTION: 73 | case Variant::COLOR: 74 | case Variant::STRING_NAME: 75 | case Variant::NODE_PATH: 76 | case Variant::RID: 77 | case Variant::OBJECT: 78 | case Variant::CALLABLE: 79 | case Variant::SIGNAL: { 80 | return p_value.stringify(); 81 | } break; 82 | default: { 83 | return p_value; 84 | } break; 85 | } 86 | } 87 | 88 | } //namespace sentry::android 89 | -------------------------------------------------------------------------------- /src/sentry/android/android_event.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_ANDROID_EVENT_H 2 | #define SENTRY_ANDROID_EVENT_H 3 | 4 | #include "sentry/sentry_event.h" 5 | 6 | using namespace godot; 7 | 8 | namespace sentry::android { 9 | 10 | // Event class that is used with the AndroidSDK. 11 | class AndroidEvent : public SentryEvent { 12 | GDCLASS(AndroidEvent, SentryEvent); 13 | 14 | private: 15 | Object *android_plugin = nullptr; 16 | int32_t event_handle; 17 | bool is_borrowed = false; 18 | 19 | protected: 20 | static void _bind_methods() {} 21 | 22 | public: 23 | int32_t get_handle() { return event_handle; } 24 | 25 | virtual String get_id() const override; 26 | 27 | virtual void set_message(const String &p_message) override; 28 | virtual String get_message() const override; 29 | 30 | virtual void set_timestamp(const Ref &p_timestamp) override; 31 | virtual Ref get_timestamp() const override; 32 | 33 | virtual String get_platform() const override; 34 | 35 | virtual void set_level(sentry::Level p_level) override; 36 | virtual sentry::Level get_level() const override; 37 | 38 | virtual void set_logger(const String &p_logger) override; 39 | virtual String get_logger() const override; 40 | 41 | virtual void set_release(const String &p_release) override; 42 | virtual String get_release() const override; 43 | 44 | virtual void set_dist(const String &p_dist) override; 45 | virtual String get_dist() const override; 46 | 47 | virtual void set_environment(const String &p_environment) override; 48 | virtual String get_environment() const override; 49 | 50 | virtual void set_tag(const String &p_key, const String &p_value) override; 51 | virtual void remove_tag(const String &p_key) override; 52 | virtual String get_tag(const String &p_key) override; 53 | 54 | virtual void merge_context(const String &p_key, const Dictionary &p_value) override; 55 | 56 | virtual void add_exception(const Exception &p_exception) override; 57 | 58 | virtual int get_exception_count() const override; 59 | virtual void set_exception_value(int p_index, const String &p_value) override; 60 | virtual String get_exception_value(int p_index) const override; 61 | 62 | virtual bool is_crash() const override; 63 | 64 | virtual String to_json() const override; 65 | 66 | void set_as_borrowed() { is_borrowed = true; } 67 | 68 | AndroidEvent() {} 69 | AndroidEvent(Object *android_plugin, int32_t p_event_handle); 70 | virtual ~AndroidEvent() override; 71 | }; 72 | 73 | } //namespace sentry::android 74 | 75 | #endif // SENTRY_ANDROID_EVENT_H 76 | -------------------------------------------------------------------------------- /project/views/output_pane.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://dllqhtd731wtc"] 2 | 3 | [ext_resource type="Script" uid="uid://7pkoni236cwi" path="res://views/demo_output.gd" id="1_nsnxs"] 4 | 5 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_owcwq"] 6 | 7 | [node name="OutputPane" type="VBoxContainer"] 8 | offset_right = 789.0 9 | offset_bottom = 519.0 10 | size_flags_vertical = 3 11 | 12 | [node name="Header - Output" type="Label" parent="."] 13 | custom_minimum_size = Vector2(0, 40.505) 14 | layout_mode = 2 15 | text = "OUTPUT" 16 | horizontal_alignment = 1 17 | vertical_alignment = 2 18 | 19 | [node name="PanelContainer" type="PanelContainer" parent="Header - Output"] 20 | layout_mode = 1 21 | anchors_preset = 1 22 | anchor_left = 1.0 23 | anchor_right = 1.0 24 | offset_left = -131.0 25 | offset_top = 1.0 26 | offset_right = -7.0 27 | offset_bottom = 51.0 28 | grow_horizontal = 0 29 | theme_override_styles/panel = SubResource("StyleBoxEmpty_owcwq") 30 | 31 | [node name="Verbosity" type="HBoxContainer" parent="Header - Output/PanelContainer"] 32 | layout_mode = 2 33 | size_flags_horizontal = 4 34 | size_flags_vertical = 4 35 | 36 | [node name="VerbosityMenu" type="MenuButton" parent="Header - Output/PanelContainer/Verbosity"] 37 | unique_name_in_owner = true 38 | custom_minimum_size = Vector2(100, 0) 39 | layout_mode = 2 40 | text = "WARNING" 41 | flat = false 42 | 43 | [node name="DemoOutput" type="RichTextLabel" parent="."] 44 | unique_name_in_owner = true 45 | layout_mode = 2 46 | size_flags_vertical = 3 47 | bbcode_enabled = true 48 | scroll_following = true 49 | script = ExtResource("1_nsnxs") 50 | 51 | [node name="BG" type="Panel" parent="DemoOutput"] 52 | show_behind_parent = true 53 | layout_mode = 1 54 | anchors_preset = 15 55 | anchor_right = 1.0 56 | anchor_bottom = 1.0 57 | grow_horizontal = 2 58 | grow_vertical = 2 59 | mouse_filter = 2 60 | 61 | [node name="MsgCopied" type="PanelContainer" parent="DemoOutput"] 62 | unique_name_in_owner = true 63 | custom_minimum_size = Vector2(190.651, 0) 64 | layout_mode = 1 65 | anchors_preset = 8 66 | anchor_left = 0.5 67 | anchor_top = 0.5 68 | anchor_right = 0.5 69 | anchor_bottom = 0.5 70 | offset_left = -75.5 71 | offset_top = -20.0 72 | offset_right = 75.5 73 | offset_bottom = 20.0 74 | grow_horizontal = 2 75 | grow_vertical = 2 76 | 77 | [node name="Label" type="Label" parent="DemoOutput/MsgCopied"] 78 | layout_mode = 2 79 | size_flags_horizontal = 4 80 | text = "Copied to clipboard" 81 | 82 | [connection signal="meta_clicked" from="DemoOutput" to="DemoOutput" method="_on_meta_clicked"] 83 | -------------------------------------------------------------------------------- /src/sentry/cocoa/cocoa_util.h: -------------------------------------------------------------------------------- 1 | #ifndef COCOA_UTIL_H 2 | #define COCOA_UTIL_H 3 | 4 | #include "cocoa_includes.h" 5 | #include "sentry/level.h" 6 | 7 | #include 8 | 9 | namespace sentry::cocoa { 10 | 11 | _FORCE_INLINE_ objc::SentryLevel sentry_level_to_objc(sentry::Level p_level) { 12 | switch (p_level) { 13 | case sentry::Level::LEVEL_DEBUG: 14 | return kSentryLevelDebug; 15 | case sentry::Level::LEVEL_INFO: 16 | return kSentryLevelInfo; 17 | case sentry::Level::LEVEL_WARNING: 18 | return kSentryLevelWarning; 19 | case sentry::Level::LEVEL_ERROR: 20 | return kSentryLevelError; 21 | case sentry::Level::LEVEL_FATAL: 22 | return kSentryLevelFatal; 23 | default: 24 | return kSentryLevelError; 25 | } 26 | } 27 | 28 | _FORCE_INLINE_ sentry::Level sentry_level_from_objc(objc::SentryLevel p_level) { 29 | switch (p_level) { 30 | case kSentryLevelDebug: 31 | return sentry::Level::LEVEL_DEBUG; 32 | case kSentryLevelInfo: 33 | return sentry::Level::LEVEL_INFO; 34 | case kSentryLevelWarning: 35 | return sentry::Level::LEVEL_WARNING; 36 | case kSentryLevelError: 37 | return sentry::Level::LEVEL_ERROR; 38 | case kSentryLevelFatal: 39 | return sentry::Level::LEVEL_FATAL; 40 | default: 41 | return sentry::Level::LEVEL_ERROR; 42 | } 43 | } 44 | 45 | _FORCE_INLINE_ NSString *string_to_objc(const godot::String &p_str) { 46 | return [NSString stringWithUTF8String:p_str.utf8()]; 47 | } 48 | 49 | _FORCE_INLINE_ NSString *string_to_objc_or_nil_if_empty(const godot::String &p_str) { 50 | return p_str.is_empty() ? nil : [NSString stringWithUTF8String:p_str.utf8()]; 51 | } 52 | 53 | _FORCE_INLINE_ godot::String string_from_objc(NSString *p_str) { 54 | return p_str ? godot::String::utf8([p_str UTF8String]) : godot::String(); 55 | } 56 | 57 | _FORCE_INLINE_ NSNumber *int_to_objc(int p_num) { 58 | return [NSNumber numberWithInt:p_num]; 59 | } 60 | 61 | _FORCE_INLINE_ NSNumber *uint64_to_objc(uint64_t p_num) { 62 | return [NSNumber numberWithUnsignedLongLong:p_num]; 63 | } 64 | 65 | _FORCE_INLINE_ NSNumber *bool_to_objc(bool p_flag) { 66 | return [NSNumber numberWithBool:p_flag]; 67 | } 68 | 69 | _FORCE_INLINE_ NSNumber *double_to_objc(double p_num) { 70 | return [NSNumber numberWithDouble:p_num]; 71 | } 72 | 73 | NSObject *variant_to_objc(const godot::Variant &p_value, int p_depth = 0); 74 | godot::Variant variant_from_objc(const NSObject *p_object); 75 | 76 | NSDictionary *dictionary_to_objc(const godot::Dictionary &p_dictionary); 77 | NSArray *string_array_to_objc(const godot::PackedStringArray &p_array); 78 | 79 | } //namespace sentry::cocoa 80 | 81 | #endif // COCOA_UTIL_H 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sentry for Godot Engine 2 | 3 | Sentry for Godot helps you monitor your game's health during QA and after release. It provides insights into crash reports, script errors, hardware information, user feedback, attachments and more through real-time alerting and an intuitive web dashboard. With automatic error reporting, release tracking, and AI-assisted analysis, you can quickly identify, prioritize, and resolve issues for a better player experience. 4 | 5 | **Architecture**: Built as a C++ GDExtension library using mature Sentry SDKs like [sentry-native](https://github.com/getsentry/sentry-native) as the foundation. 6 | 7 | ![issue-example](./.github/issue_example.png) 8 | 9 | **Feedback Welcome**: We'd love to hear about your experience! Share feedback and ask questions in [Discussions](https://github.com/getsentry/sentry-godot/discussions). 10 | 11 | 12 | ## Minimum supported Godot Engine 13 | 14 | | SDK Version | Required Godot Version | 15 | |----------------|------------------------| 16 | | `1.x` releases | Godot 4.5 or later | 17 | | `0.x` releases | Godot 4.3 or later | 18 | 19 | For upgrade instructions between major versions, see the [Migration Guide](https://docs.sentry.io/platforms/godot/migration/). 20 | 21 | ## Supported platforms and architectures 22 | 23 | - **Windows**: x86_64, x86_32 24 | - **Linux**: x86_64, x86_32 25 | - **macOS**: universal (Intel and Apple Silicon) 26 | - **Android**: arm64, arm32, x86_64 27 | - **iOS**: device and simulator 28 | - **Web**: expected in Q4 2025, after 1.0.0 stable 29 | - **W4 console forks**: coming in 2026 30 | 31 | Support for additional platforms and architectures may be added in future releases. 32 | 33 | ## Getting started 34 | 35 | Pre-built extension libraries with the demo project are available in [**Releases**](https://github.com/getsentry/sentry-godot/releases). 36 | 37 | Check the official [Sentry SDK documentation](https://docs.sentry.io/platforms/godot/) to get started. 38 | 39 | In the Godot editor, you can adjust options by going to `Project Settings -> Sentry -> Options`. Feel free to explore the demo `project/` for usage examples. 40 | 41 | ## Building from source 42 | 43 | For build instructions, see our [**Contributing Guide**](https://github.com/getsentry/sentry-godot/blob/master/CONTRIBUTING.md#building-sdk). 44 | 45 | ## Contributing 46 | 47 | We appreciate your contributions! Feel free to open issues for feature requests and ask questions in [**Discussions**](https://github.com/getsentry/sentry-godot/discussions). Your feedback is very much welcome! 48 | 49 | Check out our [**Contributing Guide**](https://github.com/getsentry/sentry-godot/blob/master/CONTRIBUTING.md). 50 | -------------------------------------------------------------------------------- /src/sentry/util/simple_bind.h: -------------------------------------------------------------------------------- 1 | #ifndef SIMPLE_BIND_H 2 | #define SIMPLE_BIND_H 3 | 4 | #include 5 | 6 | // This file contains macros to simplify the binding of properties from a C++ class to the Godot ClassDB, 7 | // which makes them accessible from GDScript and the Godot editor. 8 | 9 | namespace sentry::bind { 10 | 11 | // Registers the specified property and its accessors in the ClassDB. 12 | // Note: This function is used in the macros defined below. 13 | template 14 | _FORCE_INLINE_ void bind_property(const godot::StringName &p_class, const godot::PropertyInfo &p_info, const godot::StringName &p_setter_name, const godot::StringName &p_getter_name) { 15 | godot::ClassDB::bind_method(D_METHOD(p_setter_name, p_info.name), Setter); 16 | godot::ClassDB::bind_method(D_METHOD(p_getter_name), Getter); 17 | godot::ClassDB::add_property(p_class, p_info, p_setter_name, p_getter_name); 18 | } 19 | 20 | // Registers read-only property and its getter in the ClassDB. 21 | template 22 | _FORCE_INLINE_ void bind_property_readonly(const godot::StringName &p_class, const godot::PropertyInfo &p_info, const godot::StringName &p_getter_name) { 23 | godot::ClassDB::bind_method(D_METHOD(p_getter_name), Getter); 24 | godot::ClassDB::add_property(p_class, p_info, "", p_getter_name); 25 | } 26 | 27 | } //namespace sentry::bind 28 | 29 | // Macro to bind a property and its getter and setter. 30 | #define BIND_PROPERTY(m_class, m_prop_info, m_setter, m_getter) \ 31 | sentry::bind::bind_property<&m_class::m_setter, &m_class::m_getter>(m_class::get_class_static(), m_prop_info, #m_setter, #m_getter) 32 | 33 | // A simplified version of the previous macro, for basic cases. 34 | #define BIND_PROPERTY_SIMPLE(m_class, m_type, m_property) \ 35 | sentry::bind::bind_property<&m_class::set_##m_property, &m_class::get_##m_property>(m_class::get_class_static(), PropertyInfo(m_type, #m_property), "set_" #m_property, "get_" #m_property) 36 | 37 | // Macro to bind a read-only property and its getter. 38 | #define BIND_PROPERTY_READONLY(m_class, m_prop_info, m_getter) \ 39 | sentry::bind::bind_property_readonly<&m_class::m_getter>(m_class::get_class_static(), m_prop_info, #m_getter) 40 | 41 | // Macro to define a simple property with accessors and a default value. 42 | // Note: The property will be compatible with BIND_PROPERTY_SIMPLE macro. 43 | #define SIMPLE_PROPERTY(m_type, m_property, m_default) \ 44 | m_type m_property = m_default; \ 45 | void set_##m_property(m_type p_value) { m_property = p_value; } \ 46 | m_type get_##m_property() { return m_property; } 47 | 48 | #endif // SIMPLE_BIND_H 49 | -------------------------------------------------------------------------------- /src/sentry/android/android_breadcrumb.cpp: -------------------------------------------------------------------------------- 1 | #include "android_breadcrumb.h" 2 | 3 | #include "android_string_names.h" 4 | 5 | namespace sentry::android { 6 | 7 | void AndroidBreadcrumb::set_message(const String &p_message) { 8 | ERR_FAIL_NULL(android_plugin); 9 | android_plugin->call(ANDROID_SN(breadcrumbSetMessage), handle, p_message); 10 | } 11 | 12 | String AndroidBreadcrumb::get_message() const { 13 | ERR_FAIL_NULL_V(android_plugin, String()); 14 | return android_plugin->call(ANDROID_SN(breadcrumbGetMessage), handle); 15 | } 16 | 17 | void AndroidBreadcrumb::set_category(const String &p_category) { 18 | ERR_FAIL_NULL(android_plugin); 19 | android_plugin->call(ANDROID_SN(breadcrumbSetCategory), handle, p_category); 20 | } 21 | 22 | String AndroidBreadcrumb::get_category() const { 23 | ERR_FAIL_NULL_V(android_plugin, String()); 24 | return android_plugin->call(ANDROID_SN(breadcrumbGetCategory), handle); 25 | } 26 | 27 | void AndroidBreadcrumb::set_level(sentry::Level p_level) { 28 | ERR_FAIL_NULL(android_plugin); 29 | android_plugin->call(ANDROID_SN(breadcrumbSetLevel), handle, p_level); 30 | } 31 | 32 | sentry::Level AndroidBreadcrumb::get_level() const { 33 | ERR_FAIL_NULL_V(android_plugin, sentry::Level::LEVEL_INFO); 34 | Variant result = android_plugin->call(ANDROID_SN(breadcrumbGetLevel), handle); 35 | ERR_FAIL_COND_V(result.get_type() != Variant::INT, sentry::Level::LEVEL_INFO); 36 | return sentry::int_to_level(result); 37 | } 38 | 39 | void AndroidBreadcrumb::set_type(const String &p_type) { 40 | ERR_FAIL_NULL(android_plugin); 41 | android_plugin->call(ANDROID_SN(breadcrumbSetType), handle, p_type); 42 | } 43 | 44 | String AndroidBreadcrumb::get_type() const { 45 | ERR_FAIL_NULL_V(android_plugin, String()); 46 | return android_plugin->call(ANDROID_SN(breadcrumbGetType), handle); 47 | } 48 | 49 | void AndroidBreadcrumb::set_data(const Dictionary &p_data) { 50 | ERR_FAIL_NULL(android_plugin); 51 | android_plugin->call(ANDROID_SN(breadcrumbSetData), handle, p_data); 52 | } 53 | 54 | Ref AndroidBreadcrumb::get_timestamp() { 55 | ERR_FAIL_NULL_V(android_plugin, nullptr); 56 | int64_t micros = android_plugin->call(ANDROID_SN(breadcrumbGetTimestamp), handle); 57 | return SentryTimestamp::from_microseconds_since_unix_epoch(micros); 58 | } 59 | 60 | AndroidBreadcrumb::AndroidBreadcrumb(Object *p_android_plugin, int32_t p_breadcrumb_handle) : 61 | android_plugin(p_android_plugin), handle(p_breadcrumb_handle) { 62 | ERR_FAIL_NULL(p_android_plugin); 63 | } 64 | 65 | AndroidBreadcrumb::~AndroidBreadcrumb() { 66 | if (android_plugin) { 67 | android_plugin->call(ANDROID_SN(releaseBreadcrumb), handle); 68 | } 69 | } 70 | 71 | } //namespace sentry::android 72 | -------------------------------------------------------------------------------- /src/sentry/sentry_event.h: -------------------------------------------------------------------------------- 1 | #ifndef SENTRY_EVENT_H 2 | #define SENTRY_EVENT_H 3 | 4 | #include "sentry/level.h" 5 | #include "sentry/sentry_timestamp.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace godot; 13 | 14 | namespace sentry { 15 | 16 | // Base class for event objects in the public API. 17 | class SentryEvent : public RefCounted { 18 | GDCLASS(SentryEvent, RefCounted); 19 | 20 | public: 21 | // Represents a frame of a stack trace. 22 | struct StackFrame { 23 | String filename; 24 | String function; 25 | int lineno = -1; 26 | bool in_app = true; 27 | String platform; 28 | String context_line; 29 | Vector> vars; 30 | PackedStringArray pre_context; 31 | PackedStringArray post_context; 32 | }; 33 | 34 | struct Exception { 35 | String type; 36 | String value; 37 | Vector frames; 38 | }; 39 | 40 | protected: 41 | static void _bind_methods(); 42 | 43 | public: 44 | virtual String get_id() const = 0; 45 | 46 | virtual void set_message(const String &p_message) = 0; 47 | virtual String get_message() const = 0; 48 | 49 | virtual void set_timestamp(const Ref &p_timestamp) = 0; 50 | virtual Ref get_timestamp() const = 0; 51 | 52 | virtual String get_platform() const = 0; 53 | 54 | virtual void set_level(sentry::Level p_level) = 0; 55 | virtual sentry::Level get_level() const = 0; 56 | 57 | virtual void set_logger(const String &p_logger) = 0; 58 | virtual String get_logger() const = 0; 59 | 60 | virtual void set_release(const String &p_release) = 0; 61 | virtual String get_release() const = 0; 62 | 63 | virtual void set_dist(const String &p_dist) = 0; 64 | virtual String get_dist() const = 0; 65 | 66 | virtual void set_environment(const String &p_environment) = 0; 67 | virtual String get_environment() const = 0; 68 | 69 | virtual void set_tag(const String &p_key, const String &p_value) = 0; 70 | virtual void remove_tag(const String &p_key) = 0; 71 | virtual String get_tag(const String &p_key) = 0; 72 | 73 | virtual void merge_context(const String &p_key, const Dictionary &p_value) = 0; 74 | 75 | virtual void add_exception(const Exception &p_exception) = 0; 76 | 77 | virtual int get_exception_count() const = 0; 78 | virtual void set_exception_value(int p_index, const String &p_value) = 0; 79 | virtual String get_exception_value(int p_index) const = 0; 80 | 81 | virtual bool is_crash() const = 0; 82 | 83 | virtual String to_json() const = 0; 84 | 85 | virtual ~SentryEvent() = default; 86 | }; 87 | 88 | } // namespace sentry 89 | 90 | #endif // SENTRY_EVENT_H 91 | -------------------------------------------------------------------------------- /src/sentry/disabled/disabled_event.h: -------------------------------------------------------------------------------- 1 | #ifndef DISABLED_EVENT_H 2 | #define DISABLED_EVENT_H 3 | 4 | #include "sentry/sentry_event.h" 5 | 6 | namespace sentry { 7 | 8 | // Event class that is used when the SDK is disabled. 9 | class DisabledEvent : public SentryEvent { 10 | GDCLASS(DisabledEvent, SentryEvent); 11 | 12 | private: 13 | String message; 14 | Ref timestamp; 15 | String logger; 16 | sentry::Level level = sentry::Level::LEVEL_INFO; 17 | String release; 18 | String dist; 19 | String environment; 20 | 21 | protected: 22 | static void _bind_methods() {} 23 | 24 | public: 25 | virtual String get_id() const override { return ""; } 26 | 27 | virtual void set_message(const String &p_message) override { message = p_message; } 28 | virtual String get_message() const override { return message; } 29 | 30 | virtual void set_timestamp(const Ref &p_timestamp) override { timestamp = p_timestamp; } 31 | virtual Ref get_timestamp() const override { return timestamp; } 32 | 33 | virtual String get_platform() const override { return ""; } 34 | 35 | virtual void set_level(sentry::Level p_level) override { level = p_level; } 36 | virtual sentry::Level get_level() const override { return level; } 37 | 38 | virtual void set_logger(const String &p_logger) override { logger = p_logger; } 39 | virtual String get_logger() const override { return logger; } 40 | 41 | virtual void set_release(const String &p_release) override { release = p_release; } 42 | virtual String get_release() const override { return release; } 43 | 44 | virtual void set_dist(const String &p_dist) override { dist = p_dist; } 45 | virtual String get_dist() const override { return dist; } 46 | 47 | virtual void set_environment(const String &p_environment) override { environment = p_environment; } 48 | virtual String get_environment() const override { return environment; } 49 | 50 | virtual void set_tag(const String &p_key, const String &p_value) override {} 51 | virtual void remove_tag(const String &p_key) override {} 52 | virtual String get_tag(const String &p_key) override { return ""; } 53 | 54 | virtual void merge_context(const String &p_key, const Dictionary &p_value) override {} 55 | 56 | virtual void add_exception(const Exception &p_exception) override {} 57 | 58 | virtual int get_exception_count() const override { return 0; } 59 | virtual void set_exception_value(int p_index, const String &p_value) override {} 60 | virtual String get_exception_value(int p_index) const override { return String(); } 61 | 62 | virtual bool is_crash() const override { return false; } 63 | 64 | virtual String to_json() const override { return ""; } 65 | }; 66 | 67 | } // namespace sentry 68 | 69 | #endif // DISABLED_EVENT_H 70 | -------------------------------------------------------------------------------- /.github/workflows/test_integration.yml: -------------------------------------------------------------------------------- 1 | name: 🧪 Integration tests 2 | 3 | on: 4 | workflow_call: 5 | secrets: 6 | SENTRY_API_TOKEN: 7 | required: true 8 | SAUCE_USERNAME: 9 | required: true 10 | SAUCE_ACCESS_KEY: 11 | required: true 12 | 13 | permissions: 14 | contents: read 15 | 16 | jobs: 17 | integration-tests: 18 | name: Test ${{matrix.test-platform}} ${{matrix.godot-arch}} 19 | runs-on: ${{matrix.runner}} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | include: 24 | - test-platform: Linux 25 | runner: ubuntu-latest 26 | godot-arch: x86_64 27 | - test-platform: Linux 28 | runner: ubuntu-latest 29 | godot-arch: x86_32 30 | - test-platform: Windows 31 | runner: windows-latest 32 | godot-arch: x86_64 33 | - test-platform: Windows 34 | runner: windows-latest 35 | godot-arch: x86_32 36 | - test-platform: macOS 37 | runner: macos-latest 38 | godot-arch: universal 39 | - test-platform: AndroidSauceLabs 40 | runner: ubuntu-latest 41 | godot-arch: x86_64 42 | test-executable: ./exports/android.apk 43 | steps: 44 | - name: Checkout repo 45 | uses: actions/checkout@v4 46 | with: 47 | submodules: false # don't initialize submodules automatically 48 | 49 | - name: Prepare testing 50 | uses: ./.github/actions/prepare-testing 51 | timeout-minutes: 15 52 | with: 53 | arch: ${{ matrix.godot-arch }} 54 | android: ${{ matrix.test-platform == 'AndroidSauceLabs' && 'true' || 'false' }} 55 | 56 | - name: Export project 57 | if: matrix.test-platform == 'AndroidSauceLabs' 58 | timeout-minutes: 10 59 | shell: bash 60 | run: | 61 | "${GODOT}" --verbose --headless --disable-crash-handler --path project --install-android-build-template --export-debug "Android Tests" "${GITHUB_WORKSPACE}/exports/android.apk" || true 62 | 63 | - name: Run integration tests 64 | shell: pwsh 65 | env: 66 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_API_TOKEN }} 67 | SENTRY_TEST_PLATFORM: ${{ matrix.test-platform }} 68 | SENTRY_TEST_EXECUTABLE: ${{ matrix.test-executable }} 69 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 70 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 71 | SAUCE_REGION: us-west-1 72 | SAUCE_DEVICE_NAME: Samsung_Galaxy_S23.* 73 | SAUCE_SESSION_NAME: Godot ${{matrix.test-platform}} E2E Tests 74 | run: Invoke-Pester -Script integration_tests/Integration.Tests.ps1 75 | -------------------------------------------------------------------------------- /project/test/suites/test_breadcrumb.gd: -------------------------------------------------------------------------------- 1 | extends SentryTestSuite 2 | ## Test SentryBreadcrumb class properties and methods. 3 | 4 | 5 | ## Test string properties accessors and UTF-8 encoding preservation. 6 | @warning_ignore("unused_parameter") 7 | func test_string_properties_and_utf8(property: String, test_parameters := [ 8 | ["message"], 9 | ["category"], 10 | ["type"], 11 | ]) -> void: 12 | var crumb := SentryBreadcrumb.create() 13 | crumb.set(property, "Hello, World!") 14 | assert_str(crumb.get(property)).is_equal("Hello, World!") 15 | crumb.set(property, "Hello 世界! 👋") 16 | assert_str(crumb.get(property)).is_equal("Hello 世界! 👋") 17 | 18 | 19 | func test_breadcrumb_create_with_message() -> void: 20 | var crumb := SentryBreadcrumb.create("test-message") 21 | assert_str(crumb.message).is_equal("test-message") 22 | 23 | 24 | func test_breadcrumb_level() -> void: 25 | var crumb := SentryBreadcrumb.create() 26 | crumb.level = SentrySDK.LEVEL_DEBUG 27 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_DEBUG) 28 | crumb.level = SentrySDK.LEVEL_INFO 29 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_INFO) 30 | crumb.level = SentrySDK.LEVEL_WARNING 31 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_WARNING) 32 | crumb.level = SentrySDK.LEVEL_ERROR 33 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_ERROR) 34 | crumb.level = SentrySDK.LEVEL_FATAL 35 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_FATAL) 36 | 37 | 38 | func test_breadcrumb_can_set_data() -> void: 39 | var crumb := SentryBreadcrumb.create() 40 | crumb.set_data({"biome": "forest", "time_of_day": 0.42}) 41 | 42 | 43 | func test_breadcrumb_default_values() -> void: 44 | var crumb := SentryBreadcrumb.create() 45 | assert_str(crumb.message).is_empty() 46 | assert_str(crumb.type).is_empty() 47 | assert_int(crumb.level).is_equal(SentrySDK.LEVEL_INFO) 48 | assert_bool(crumb.category in ["", "default"]).is_true() 49 | 50 | 51 | func test_breadcrumb_timestamp_is_set_automatically() -> void: 52 | const time_tolerance: float = 0.001 # 1 ms 53 | 54 | var time_before: float = Time.get_unix_time_from_system() - time_tolerance 55 | await get_tree().process_frame # small delay to ensure timestamp differs 56 | 57 | var crumb := SentryBreadcrumb.create() 58 | 59 | await get_tree().process_frame 60 | var time_after: float = Time.get_unix_time_from_system() + time_tolerance 61 | 62 | # Timestamp should be between time_before and time_after 63 | var timestamp: SentryTimestamp = crumb.get_timestamp() 64 | assert_object(timestamp).is_not_null() 65 | var micros: int = timestamp.microseconds_since_unix_epoch 66 | var timestamp_unix: float = float(micros) / 1_000_000.0 67 | assert_float(timestamp_unix).is_greater_equal(time_before) 68 | assert_float(timestamp_unix).is_less_equal(time_after) 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug Report 2 | description: Report a bug in Sentry for Godot 3 | labels: 4 | - Bug 5 | - Godot 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | - Write a descriptive title above. 11 | 12 | - type: input 13 | attributes: 14 | label: How do you use Sentry? 15 | description: Sentry SaaS (sentry.io) or self-hosted/on-premise (which version?) 16 | placeholder: Sentry SaaS 17 | validations: 18 | required: true 19 | 20 | - type: input 21 | attributes: 22 | label: Sentry SDK version 23 | description: | 24 | - Specify Sentry SDK version. 25 | - You can check the version in the Godot Output panel or in the log file. 26 | placeholder: 0.6.0 27 | validations: 28 | required: true 29 | 30 | - type: dropdown 31 | attributes: 32 | label: How did you install the SDK? 33 | options: 34 | - GitHub release 35 | - AssetLib 36 | - Built from source 37 | default: 0 38 | validations: 39 | required: true 40 | 41 | - type: input 42 | attributes: 43 | label: Godot version 44 | description: | 45 | - Specify the Godot version and hardware info if relevant. 46 | - You can copy the version info by clicking on it in the Godot status bar. 47 | placeholder: v4.4.1.stable.official [49a5bc7b6] 48 | validations: 49 | required: true 50 | 51 | - type: dropdown 52 | attributes: 53 | label: Which platform? 54 | options: 55 | - Windows 56 | - macOS 57 | - Linux 58 | - Android 59 | - iOS 60 | default: 0 61 | validations: 62 | required: true 63 | 64 | - type: textarea 65 | attributes: 66 | label: How to reproduce 67 | description: | 68 | - Provide a list of steps or sample code that reproduces the issue. 69 | - You can provide a small Godot project which reproduces the issue. 70 | - Drag and drop a ZIP archive to upload it (10Mb limit). 71 | - Don't include the `.godot` folder in the archive. 72 | - Don't include the `addons/sentry` folder in the archive. 73 | - Reproduction project helps us find the bug faster! 74 | validations: 75 | required: false 76 | 77 | - type: textarea 78 | attributes: 79 | label: Expected result 80 | description: | 81 | - What you thought would happen. 82 | validations: 83 | required: false 84 | 85 | - type: textarea 86 | attributes: 87 | label: Actual result 88 | description: | 89 | - What actually happened. 90 | - Maybe a screenshot/recording? 91 | - Maybe some logs? 92 | validations: 93 | required: false 94 | -------------------------------------------------------------------------------- /src/sentry/android/android_log.cpp: -------------------------------------------------------------------------------- 1 | #include "android_log.h" 2 | 3 | #include "android_string_names.h" 4 | 5 | namespace sentry::android { 6 | 7 | LogLevel AndroidLog::get_level() const { 8 | ERR_FAIL_NULL_V(android_plugin, LOG_LEVEL_INFO); 9 | Variant result = android_plugin->call(ANDROID_SN(logGetLevel), handle); 10 | ERR_FAIL_COND_V(result.get_type() != Variant::INT, LOG_LEVEL_INFO); 11 | return (LogLevel)(int)result; 12 | } 13 | 14 | void AndroidLog::set_level(LogLevel p_level) { 15 | ERR_FAIL_NULL(android_plugin); 16 | android_plugin->call(ANDROID_SN(logSetLevel), handle, p_level); 17 | } 18 | 19 | String AndroidLog::get_body() const { 20 | ERR_FAIL_NULL_V(android_plugin, String()); 21 | return android_plugin->call(ANDROID_SN(logGetBody), handle); 22 | } 23 | 24 | void AndroidLog::set_body(const String &p_body) { 25 | ERR_FAIL_NULL(android_plugin); 26 | android_plugin->call(ANDROID_SN(logSetBody), handle, p_body); 27 | } 28 | 29 | Variant AndroidLog::get_attribute(const String &p_name) const { 30 | ERR_FAIL_NULL_V(android_plugin, Variant()); 31 | Dictionary result = android_plugin->call(ANDROID_SN(logGetAttribute), handle, p_name); 32 | return result["value"]; 33 | } 34 | 35 | void AndroidLog::set_attribute(const String &p_name, const Variant &p_value) { 36 | ERR_FAIL_NULL(android_plugin); 37 | 38 | Dictionary attr_data; 39 | attr_data["name"] = p_name; 40 | 41 | switch (p_value.get_type()) { 42 | case Variant::BOOL: { 43 | attr_data["value"] = p_value; 44 | attr_data["type"] = "boolean"; 45 | } break; 46 | case Variant::INT: { 47 | attr_data["value"] = p_value; 48 | attr_data["type"] = "integer"; 49 | } break; 50 | case Variant::FLOAT: { 51 | attr_data["value"] = p_value; 52 | attr_data["type"] = "double"; 53 | } break; 54 | default: { 55 | attr_data["value"] = p_value.stringify(); 56 | attr_data["type"] = "string"; 57 | } break; 58 | } 59 | 60 | android_plugin->call(ANDROID_SN(logSetAttribute), handle, attr_data); 61 | } 62 | 63 | void AndroidLog::add_attributes(const Dictionary &p_attributes) { 64 | ERR_FAIL_NULL(android_plugin); 65 | android_plugin->call(ANDROID_SN(logAddAttributes), handle, p_attributes); 66 | } 67 | 68 | void AndroidLog::remove_attribute(const String &p_name) { 69 | ERR_FAIL_NULL(android_plugin); 70 | android_plugin->call(ANDROID_SN(logRemoveAttribute), handle, p_name); 71 | } 72 | 73 | AndroidLog::AndroidLog() { 74 | // NOTE: Required for ClassDB registration and compilation to succeed. 75 | ERR_PRINT("This constructor is not intended for runtime use."); 76 | } 77 | 78 | AndroidLog::AndroidLog(Object *p_android_plugin, int32_t p_handle) : 79 | android_plugin(p_android_plugin), handle(p_handle) { 80 | } 81 | 82 | AndroidLog::~AndroidLog() { 83 | if (android_plugin && !is_borrowed) { 84 | android_plugin->call(ANDROID_SN(releaseLog), handle); 85 | } 86 | } 87 | 88 | } //namespace sentry::android 89 | -------------------------------------------------------------------------------- /src/editor/sentry_editor_export_plugin_unix.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_editor_export_plugin_unix.h" 2 | 3 | #if defined(TOOLS_ENABLED) && !defined(WINDOWS_ENABLED) 4 | 5 | #include "sentry/logging/print.h" 6 | 7 | #include 8 | #include 9 | 10 | namespace { 11 | 12 | void _set_executable_permissions(const String &p_path) { 13 | if (!FileAccess::file_exists(p_path)) { 14 | return; 15 | } 16 | 17 | BitField perm = FileAccess::get_unix_permissions(p_path); 18 | 19 | // Permissions for executable files should be set to 755. 20 | BitField new_perm = 21 | FileAccess::UNIX_READ_OWNER | FileAccess::UNIX_WRITE_OWNER | FileAccess::UNIX_EXECUTE_OWNER | 22 | FileAccess::UNIX_READ_GROUP | FileAccess::UNIX_EXECUTE_GROUP | 23 | FileAccess::UNIX_READ_OTHER | FileAccess::UNIX_EXECUTE_OTHER; 24 | 25 | if (perm != new_perm) { 26 | Error err = FileAccess::set_unix_permissions(p_path, new_perm); 27 | if (err != OK && err != ERR_UNAVAILABLE) { 28 | sentry::logging::print_warning("Failed to set executable permissions for: " + p_path); 29 | } 30 | } 31 | } 32 | 33 | void _find_and_fix_crashpad_handler(const String &p_path) { 34 | List dirs_to_process; 35 | dirs_to_process.push_back(FileAccess::file_exists(p_path) ? p_path.get_base_dir() : p_path); 36 | 37 | while (!dirs_to_process.is_empty()) { 38 | String work_dir = dirs_to_process.back()->get(); 39 | dirs_to_process.pop_back(); 40 | 41 | Ref da = DirAccess::open(work_dir); 42 | if (da.is_null()) { 43 | continue; 44 | } 45 | 46 | da->list_dir_begin(); 47 | 48 | String file = da->get_next(); 49 | while (!file.is_empty()) { 50 | String full_path = work_dir.path_join(file); 51 | 52 | if (da->current_is_dir() && file != "." && file != "..") { 53 | // Add subdirectory to be processed 54 | dirs_to_process.push_back(full_path); 55 | } else if (file == "crashpad_handler") { 56 | _set_executable_permissions(full_path); 57 | } 58 | 59 | file = da->get_next(); 60 | } 61 | da->list_dir_end(); 62 | } 63 | } 64 | 65 | } // unnamed namespace 66 | 67 | String SentryEditorExportPluginUnix::_get_name() const { 68 | return "SentryEditorExportPluginUnix"; 69 | } 70 | 71 | bool SentryEditorExportPluginUnix::_supports_platform(const Ref &p_platform) const { 72 | return p_platform->get_os_name() != "Windows"; 73 | } 74 | 75 | void SentryEditorExportPluginUnix::_export_begin(const PackedStringArray &p_features, bool p_is_debug, const String &p_path, uint32_t p_flags) { 76 | export_path = p_path; 77 | } 78 | 79 | void SentryEditorExportPluginUnix::_export_end() { 80 | // Fix crashpad handler executable permissions on Unix platforms. 81 | _find_and_fix_crashpad_handler(export_path); 82 | } 83 | 84 | #endif // TOOLS_ENABLED && !WINDOWS_ENABLED 85 | -------------------------------------------------------------------------------- /src/sentry/native/native_breadcrumb.cpp: -------------------------------------------------------------------------------- 1 | #include "native_breadcrumb.h" 2 | 3 | #include "godot_cpp/core/error_macros.hpp" 4 | #include "sentry/native/native_util.h" 5 | 6 | #include 7 | 8 | namespace sentry::native { 9 | 10 | void NativeBreadcrumb::set_message(const String &p_message) { 11 | sentry::native::sentry_value_set_or_remove_string_by_key(native_crumb, "message", p_message); 12 | } 13 | 14 | String NativeBreadcrumb::get_message() const { 15 | return String::utf8(sentry_value_as_string( 16 | sentry_value_get_by_key(native_crumb, "message"))); 17 | } 18 | 19 | void NativeBreadcrumb::set_category(const String &p_category) { 20 | sentry::native::sentry_value_set_or_remove_string_by_key(native_crumb, "category", p_category); 21 | } 22 | 23 | String NativeBreadcrumb::get_category() const { 24 | return String::utf8(sentry_value_as_string( 25 | sentry_value_get_by_key(native_crumb, "category"))); 26 | } 27 | 28 | void NativeBreadcrumb::set_level(sentry::Level p_level) { 29 | sentry_value_set_by_key(native_crumb, "level", 30 | sentry_value_new_string(sentry::native::level_to_cstring(p_level))); 31 | } 32 | 33 | sentry::Level NativeBreadcrumb::get_level() const { 34 | sentry_value_t value = sentry_value_get_by_key(native_crumb, "level"); 35 | if (sentry_value_is_null(value)) { 36 | return sentry::Level::LEVEL_INFO; 37 | } 38 | return sentry::native::cstring_to_level(sentry_value_as_string(value)); 39 | } 40 | 41 | void NativeBreadcrumb::set_type(const String &p_type) { 42 | sentry::native::sentry_value_set_or_remove_string_by_key(native_crumb, "type", p_type); 43 | } 44 | 45 | String NativeBreadcrumb::get_type() const { 46 | return String::utf8(sentry_value_as_string( 47 | sentry_value_get_by_key(native_crumb, "type"))); 48 | } 49 | 50 | void NativeBreadcrumb::set_data(const Dictionary &p_data) { 51 | sentry_value_t native_data = sentry::native::variant_to_sentry_value(p_data); 52 | sentry_value_set_by_key(native_crumb, "data", native_data); 53 | } 54 | 55 | Ref NativeBreadcrumb::get_timestamp() { 56 | sentry_value_t value = sentry_value_get_by_key(native_crumb, "timestamp"); 57 | return SentryTimestamp::parse_rfc3339_cstr(sentry_value_as_string(value)); 58 | } 59 | 60 | NativeBreadcrumb::NativeBreadcrumb(sentry_value_t p_native_crumb) { 61 | if (sentry_value_refcount(p_native_crumb) > 0) { 62 | sentry_value_incref(p_native_crumb); // acquire ownership 63 | native_crumb = p_native_crumb; 64 | } else { 65 | // Shouldn't happen in healthy code. 66 | native_crumb = sentry_value_new_breadcrumb(NULL, NULL); 67 | ERR_PRINT_ONCE("Sentry: Internal error: Breadcrumb refcount is zero."); 68 | } 69 | } 70 | 71 | NativeBreadcrumb::NativeBreadcrumb() { 72 | native_crumb = sentry_value_new_breadcrumb(NULL, NULL); 73 | } 74 | 75 | NativeBreadcrumb::~NativeBreadcrumb() { 76 | sentry_value_decref(native_crumb); // release ownership 77 | } 78 | 79 | } //namespace sentry::native 80 | -------------------------------------------------------------------------------- /src/sentry/sentry_logger.cpp: -------------------------------------------------------------------------------- 1 | #include "sentry_logger.h" 2 | 3 | #include "sentry/sentry_log.h" // Needed for VariantCaster 4 | #include "sentry/sentry_sdk.h" 5 | 6 | namespace sentry { 7 | 8 | void SentryLogger::log(LogLevel p_level, const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 9 | String body = p_body; 10 | Dictionary attributes; 11 | attributes.merge(p_attributes); 12 | if (!p_params.is_empty()) { 13 | attributes["sentry.message.template"] = p_body; 14 | for (int i = 0; i < p_params.size(); i++) { 15 | String attr_key = "sentry.message.parameter." + itos(i); 16 | attributes[attr_key] = p_params[i]; 17 | } 18 | body = p_body % p_params; 19 | } 20 | INTERNAL_SDK()->log(p_level, body, attributes); 21 | } 22 | 23 | void SentryLogger::trace(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 24 | log(LOG_LEVEL_TRACE, p_body, p_params, p_attributes); 25 | } 26 | 27 | void SentryLogger::debug(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 28 | log(LOG_LEVEL_DEBUG, p_body, p_params, p_attributes); 29 | } 30 | 31 | void SentryLogger::info(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 32 | log(LOG_LEVEL_INFO, p_body, p_params, p_attributes); 33 | } 34 | 35 | void SentryLogger::warn(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 36 | log(LOG_LEVEL_WARN, p_body, p_params, p_attributes); 37 | } 38 | 39 | void SentryLogger::error(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 40 | log(LOG_LEVEL_ERROR, p_body, p_params, p_attributes); 41 | } 42 | 43 | void SentryLogger::fatal(const String &p_body, const Array &p_params, const Dictionary &p_attributes) { 44 | log(LOG_LEVEL_FATAL, p_body, p_params, p_attributes); 45 | } 46 | 47 | void SentryLogger::_bind_methods() { 48 | ClassDB::bind_method(D_METHOD("log", "level", "body", "parameters", "attributes"), &SentryLogger::log, DEFVAL(Array()), DEFVAL(Dictionary())); 49 | ClassDB::bind_method(D_METHOD("trace", "body", "parameters", "attributes"), &SentryLogger::trace, DEFVAL(Array()), DEFVAL(Dictionary())); 50 | ClassDB::bind_method(D_METHOD("debug", "body", "parameters", "attributes"), &SentryLogger::debug, DEFVAL(Array()), DEFVAL(Dictionary())); 51 | ClassDB::bind_method(D_METHOD("info", "body", "parameters", "attributes"), &SentryLogger::info, DEFVAL(Array()), DEFVAL(Dictionary())); 52 | ClassDB::bind_method(D_METHOD("warn", "body", "parameters", "attributes"), &SentryLogger::warn, DEFVAL(Array()), DEFVAL(Dictionary())); 53 | ClassDB::bind_method(D_METHOD("error", "body", "parameters", "attributes"), &SentryLogger::error, DEFVAL(Array()), DEFVAL(Dictionary())); 54 | ClassDB::bind_method(D_METHOD("fatal", "body", "parameters", "attributes"), &SentryLogger::fatal, DEFVAL(Array()), DEFVAL(Dictionary())); 55 | } 56 | 57 | SentryLogger::SentryLogger() {} 58 | 59 | } //namespace sentry 60 | -------------------------------------------------------------------------------- /src/sentry/processing/view_hierarchy_builder.cpp: -------------------------------------------------------------------------------- 1 | #include "view_hierarchy_builder.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | using namespace godot; 12 | 13 | namespace { 14 | 15 | inline void _start_name_value_pair_new(sentry::util::UTF8Buffer &p_builder, const char *p_name, const String &p_value) { 16 | p_builder.append("\""); 17 | p_builder.append(p_name); 18 | p_builder.append("\":\""); 19 | p_builder.append(p_value); 20 | p_builder.append("\""); 21 | } 22 | 23 | inline void _next_name_value_pair_new(sentry::util::UTF8Buffer &p_builder, const char *p_name, const String &p_value) { 24 | p_builder.append(",\""); 25 | p_builder.append(p_name); 26 | p_builder.append("\":\""); 27 | p_builder.append(p_value); 28 | p_builder.append("\""); 29 | } 30 | 31 | } // unnamed namespace 32 | 33 | namespace sentry { 34 | 35 | sentry::util::UTF8Buffer ViewHierarchyBuilder::build_json() { 36 | SceneTree *sml = Object::cast_to(Engine::get_singleton()->get_main_loop()); 37 | ERR_FAIL_NULL_V(sml, ::sentry::util::UTF8Buffer(0)); 38 | 39 | sentry::util::UTF8Buffer buffer{ size_t(estimated_buffer_size) }; 40 | 41 | buffer.append(R"({"rendering_system":"Godot","windows":[)"); 42 | 43 | List stack; 44 | List hierarchy; 45 | 46 | if (sml->get_root()) { 47 | stack.push_back(sml->get_root()); 48 | } 49 | 50 | while (!stack.is_empty()) { 51 | if (buffer.ends_with("}")) { 52 | buffer.append(",{"); 53 | } else { 54 | buffer.append("{"); 55 | } 56 | 57 | Node *node = stack.back()->get(); 58 | stack.pop_back(); 59 | 60 | _start_name_value_pair_new(buffer, "name", node->get_name()); 61 | _next_name_value_pair_new(buffer, "class", node->get_class()); 62 | 63 | String scene_path = node->get_scene_file_path(); 64 | if (!scene_path.is_empty()) { 65 | _next_name_value_pair_new(buffer, "scene", scene_path); 66 | } 67 | 68 | const Ref