Skip to content

feat: Add user attachments support #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d4dc098
Add user attachments
limbonaut Jun 25, 2025
6ff3797
Make attachment properties readonly
limbonaut Jun 19, 2025
017cf87
Return null for empty path
limbonaut Jun 19, 2025
8929e9a
Add class reference
limbonaut Jun 19, 2025
70e4a13
Update SentryAttachment.xml
limbonaut Jun 20, 2025
4508c99
Add remove attachments on Android
limbonaut Jun 25, 2025
9568f2b
Update disabled_attachment.h
limbonaut Jun 25, 2025
39db26c
Remove attachment specializations
limbonaut Jun 25, 2025
568cd50
Add attachment.filename and rename path property
limbonaut Jun 25, 2025
2207e02
Clarify comment
limbonaut Jun 25, 2025
ac340a4
Update SentryAttachment.xml
limbonaut Jun 25, 2025
c5172a5
Remove setters
limbonaut Jun 25, 2025
6c4e679
Dont add attachments to transactions as per recommendation
limbonaut Jun 25, 2025
fe5b491
Add simple test
limbonaut Jun 25, 2025
b526f8d
Redundant call
limbonaut Jun 25, 2025
2835ce0
Merge branch 'main' into feat/user-attachments
limbonaut Jun 25, 2025
d41169b
Create/add attachments from bytes (native)
limbonaut Jun 25, 2025
a174012
Update test_attachment.gd
limbonaut Jun 25, 2025
7c8857c
Create/add attachments android
limbonaut Jun 25, 2025
09c11a6
Update native_sdk.cpp
limbonaut Jun 25, 2025
8707143
Add controls to create an attachment in the demo project
limbonaut Jun 25, 2025
9000027
Update SentryAttachment.xml
limbonaut Jun 25, 2025
43f7b35
Fix global attachments on Android
limbonaut Jun 25, 2025
5710a6e
Update doc
limbonaut Jun 25, 2025
d5d40e5
Update CHANGELOG.md
limbonaut Jun 25, 2025
fab96b6
Fix unused variable in bridge layer
limbonaut Jun 25, 2025
3a02431
Remove virtual destructor since SentryAttachment is no longer a base
limbonaut Jun 25, 2025
72843c5
Improve null safety in the bridge layer while handling attachments
limbonaut Jun 25, 2025
58da2f6
Add corrections to SentrySDK.xml
limbonaut Jun 26, 2025
eea3098
Add setters and simplify creator methods
limbonaut Jun 26, 2025
31a7519
Remove default values from creator methods
limbonaut Jun 26, 2025
8181977
Remove remove_attachment() method
limbonaut Jun 30, 2025
624b150
Add MIME type info to SentryAttachment content_type docs
limbonaut Jun 30, 2025
398a5b5
Add usage examples
limbonaut Jun 30, 2025
1bc3ed1
Merge branch 'main' into feat/user-attachments
limbonaut Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

- Initial Android support ([#169](https://github.com/getsentry/sentry-godot/pull/169))
- Refine demo for mobile screens ([#196](https://github.com/getsentry/sentry-godot/pull/196))
- Add user attachments support ([#205](https://github.com/getsentry/sentry-godot/pull/205))

## Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ class SentryAndroidGodotPlugin(godot: Godot) : GodotPlugin(godot) {
Sentry.getGlobalScope().addAttachment(attachment)
}

@UsedByGodot
fun addBytesAttachment(bytes: ByteArray, filename: String, contentType: String, attachmentType: String) {
val attachment = Attachment(
bytes,
filename,
contentType.ifEmpty { null },
attachmentType.ifEmpty { null },
false
)
Sentry.getGlobalScope().addAttachment(attachment)
}

@UsedByGodot
fun setContext(key: String, value: Dictionary) {
Sentry.getGlobalScope().setContexts(key, value)
Expand Down
60 changes: 60 additions & 0 deletions doc_classes/SentryAttachment.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="SentryAttachment" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/godotengine/godot/master/doc/class.xsd">
<brief_description>
Represents a file attachment that can be sent with Sentry events.
</brief_description>
<description>
SentryAttachment represents a file that can be attached to Sentry events to provide additional context. Attachments are files that are uploaded alongside error reports and can include log files, screenshots, configuration files, or any other relevant data.
Attachments can be created using [method SentryAttachment.create_with_path] for existing files on disk. Once created, they can be added to future events using [method SentrySDK.add_attachment]:
[codeblock]
var attachment := SentryAttachment.create_with_path("user://logs/godot.log")
attachment.content_type = "text/plain"
SentrySDK.add_attachment(attachment)
[/codeblock]
Attachments can also be created using [method SentryAttachment.create_with_bytes] for data already in memory:
[codeblock]
var bytes: PackedByteArray = "Hello, world!".to_ascii_buffer()
var attachment := SentryAttachment.create_with_bytes(bytes, "hello.txt")
attachment.content_type = "text/plain"
SentrySDK.add_attachment(attachment)
[/codeblock]
To learn more about attachments, visit [url=https://docs.sentry.io/platforms/godot/enriching-events/attachments/]Attachments documentation[/url].
</description>
<tutorials>
</tutorials>
<methods>
<method name="create_with_bytes" qualifiers="static">
<return type="SentryAttachment" />
<param index="0" name="bytes" type="PackedByteArray" />
<param index="1" name="filename" type="String" />
<description>
Creates a new [SentryAttachment] with the specified [param bytes] data and [param filename]. The [param bytes] parameter contains the raw file data to be attached. The [param filename] parameter specifies the display name for the attachment in Sentry.
This method is useful when you have file data already loaded in memory or when creating attachments from generated content rather than existing files on disk.
</description>
</method>
<method name="create_with_path" qualifiers="static">
<return type="SentryAttachment" />
<param index="0" name="path" type="String" />
<description>
Creates a new [SentryAttachment] with the specified file [param path] and optional [param filename] and [param content_type]. The [param path] should point to an existing file and supports Godot's virtual file system paths like "user://".
[b]Note:[/b] Modifying attachment properties after the attachment has been added with [method SentrySDK.add_attachment] will have no effect. To apply property changes, you need to re-add the attachment.
[b]Important:[/b] Attachments are read lazily at the time an event is sent to Sentry.
</description>
</method>
</methods>
<members>
<member name="bytes" type="PackedByteArray" setter="set_bytes" getter="get_bytes">
Contains the raw byte data of the attachment.
</member>
<member name="content_type" type="String" setter="set_content_type" getter="get_content_type">
The MIME content type of the attachment file. This helps Sentry understand how to handle and display the attachment.
Sentry understands and renders the following MIME types: [code]text/plain[/code], [code]text/css[/code], [code]text/csv[/code], [code]text/html[/code], [code]text/javascript[/code], [code]text/json[/code] or [code]text/x-json[/code] or [code]application/json[/code] or [code]application/ld+json[/code], [code]image/jpeg[/code], [code]image/png[/code], [code]image/gif[/code].
</member>
<member name="filename" type="String" setter="set_filename" getter="get_filename">
The filename of the attachment. This is the name that will be displayed in Sentry. If not provided, the filename will be extracted from the [member path].
</member>
<member name="path" type="String" setter="set_path" getter="get_path">
The file path of the attachment. This can be an absolute path or use Godot's virtual file system paths such as "user://".
</member>
</members>
</class>
8 changes: 8 additions & 0 deletions doc_classes/SentrySDK.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
<tutorials>
</tutorials>
<methods>
<method name="add_attachment">
<return type="void" />
<param index="0" name="attachment" type="SentryAttachment" />
<description>
Attaches a file to future Sentry events. The [param attachment] should be a [SentryAttachment] object created with [method SentryAttachment.create_with_path] or [method SentryAttachment.create_with_bytes]. Supports Godot's virtual file system paths like "user://".
To learn more, visit [url=https://docs.sentry.io/platforms/godot/enriching-events/attachments/]Attachments documentation[/url].
</description>
</method>
<method name="add_breadcrumb">
<return type="void" />
<param index="0" name="message" type="String" />
Expand Down
32 changes: 32 additions & 0 deletions project/test/suites/test_attachment.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
extends GdUnitTestSuite
## Basic tests for the SentryAttachment class.


func test_create_with_path() -> void:
var attachment := SentryAttachment.create_with_path("user://logs/godot.log")
attachment.filename = "logfile.txt"
attachment.content_type = "text/plain"

assert_array(attachment.bytes).is_empty()
assert_str(attachment.path).is_equal("user://logs/godot.log")
assert_str(attachment.filename).is_equal("logfile.txt")
assert_str(attachment.content_type).is_equal("text/plain")


func test_create_with_bytes() -> void:
var contents := """
Hello, world!
"""
var bytes: PackedByteArray = contents.to_utf8_buffer()

var attachment := SentryAttachment.create_with_bytes(bytes, "hello.txt")
attachment.content_type = "text/plain"

assert_array(attachment.bytes).is_not_empty()
assert_str(attachment.path).is_empty()
assert_str(attachment.filename).is_equal("hello.txt")
assert_str(attachment.content_type).is_equal("text/plain")

assert_int(attachment.bytes.size()).is_equal(bytes.size())
for i in attachment.bytes.size():
assert_int(attachment.bytes[i]).is_equal(bytes[i])
1 change: 1 addition & 0 deletions project/test/suites/test_attachment.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://b10m6784fkgyb
9 changes: 9 additions & 0 deletions project/views/enrich_events.gd
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,12 @@ func _on_set_context_pressed() -> void:
DemoOutput.print_err("Failed set context: Dictionary is expected, but found: " + type_string(typeof(result)))
else:
DemoOutput.print_err("Failed to parse expression: " + expr.get_error_text())


func _on_attach_button_pressed() -> void:
var content: String = %AttachmentContent.text
var bytes: PackedByteArray = content.to_utf8_buffer()
var attachment := SentryAttachment.create_with_bytes(bytes, "hello.txt")
attachment.content_type = "text/plain"
SentrySDK.add_attachment(attachment)
DemoOutput.print_info("Attachment added.")
19 changes: 19 additions & 0 deletions project/views/enrich_events.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ unique_name_in_owner = true
layout_mode = 2
text = "Add Tag"

[node name="Attachment" type="HBoxContainer" parent="."]
layout_mode = 2

[node name="Label" type="Label" parent="Attachment"]
layout_mode = 2
text = "Attach:"

[node name="AttachmentContent" type="LineEdit" parent="Attachment"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Hello, World!"
placeholder_text = "Content"

[node name="AttachButton" type="Button" parent="Attachment"]
layout_mode = 2
text = "Attach hello.txt"

[node name="Context" type="VBoxContainer" parent="."]
layout_mode = 2

Expand Down Expand Up @@ -97,4 +115,5 @@ text = "Add context"

[connection signal="pressed" from="Breadcrumb/AddBreadcrumbButton" to="." method="_on_add_breadcrumb_button_pressed"]
[connection signal="pressed" from="Tags/AddTagButton" to="." method="_on_add_tag_button_pressed"]
[connection signal="pressed" from="Attachment/AttachButton" to="." method="_on_attach_button_pressed"]
[connection signal="pressed" from="Context/SetContext" to="." method="_on_set_context_pressed"]
2 changes: 2 additions & 0 deletions src/register_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "sentry/processing/screenshot_processor.h"
#include "sentry/processing/view_hierarchy_processor.h"
#include "sentry/util/print.h"
#include "sentry_attachment.h"
#include "sentry_configuration.h"
#include "sentry_event.h"
#include "sentry_event_processor.h"
Expand Down Expand Up @@ -68,6 +69,7 @@ void initialize_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(SentryConfiguration);
GDREGISTER_CLASS(SentryUser);
GDREGISTER_CLASS(SentrySDK);
GDREGISTER_ABSTRACT_CLASS(SentryAttachment);
GDREGISTER_ABSTRACT_CLASS(SentryEvent);
GDREGISTER_INTERNAL_CLASS(DisabledEvent);
GDREGISTER_INTERNAL_CLASS(SentryEventProcessor);
Expand Down
33 changes: 28 additions & 5 deletions src/sentry/android/android_sdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

#include "android_event.h"
#include "android_string_names.h"
#include "sentry/common_defs.h"
#include "sentry/processing/process_event.h"
#include "sentry/util/print.h"
#include "sentry_attachment.h"

#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/project_settings.hpp>
#include <godot_cpp/variant/callable.hpp>

using namespace godot;
Expand Down Expand Up @@ -101,19 +104,39 @@ String AndroidSDK::capture_event(const Ref<SentryEvent> &p_event) {
return android_event->get_id();
}

void AndroidSDK::add_attachment(const Ref<SentryAttachment> &p_attachment) {
ERR_FAIL_COND(p_attachment.is_null());

if (p_attachment->get_path().is_empty()) {
sentry::util::print_debug("attaching bytes with filename: ", p_attachment->get_filename());
android_plugin->call(ANDROID_SN(addBytesAttachment),
p_attachment->get_bytes(),
p_attachment->get_filename(),
p_attachment->get_content_type(),
String());
} else {
String absolute_path = ProjectSettings::get_singleton()->globalize_path(p_attachment->get_path());
sentry::util::print_debug("attaching file: ", absolute_path);
android_plugin->call(ANDROID_SN(addFileAttachment),
absolute_path,
p_attachment->get_filename(),
p_attachment->get_content_type(),
String());
}
}

void AndroidSDK::initialize(const PackedStringArray &p_global_attachments) {
ERR_FAIL_NULL(android_plugin);

sentry::util::print_debug("Initializing Sentry Android SDK");

for (const String &path : p_global_attachments) {
String file = path.get_file();
bool is_view_hierarchy = file == "view-hierarchy.json";
bool is_view_hierarchy = path.ends_with(SENTRY_VIEW_HIERARCHY_FN);
android_plugin->call(ANDROID_SN(addFileAttachment),
path,
file,
is_view_hierarchy ? "application/json" : "",
is_view_hierarchy ? "event.view_hierarchy" : "");
String(), // filename
is_view_hierarchy ? "application/json" : String(),
is_view_hierarchy ? "event.view_hierarchy" : String());
}

android_plugin->call("initialize",
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/android/android_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class AndroidSDK : public InternalSDK {
virtual Ref<SentryEvent> create_event() override;
virtual String capture_event(const Ref<SentryEvent> &p_event) override;

virtual void add_attachment(const Ref<SentryAttachment> &p_attachment) override;

virtual void initialize(const PackedStringArray &p_global_attachments) override;

bool has_android_plugin() const { return android_plugin != nullptr; }
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/android/android_string_names.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ void AndroidStringNames::destroy_singleton() {

AndroidStringNames::AndroidStringNames() {
// API methods.
addFileAttachment = StringName("addFileAttachment");
setContext = StringName("setContext");
removeContext = StringName("removeContext");
setTag = StringName("setTag");
Expand All @@ -29,6 +28,8 @@ AndroidStringNames::AndroidStringNames() {
createEvent = StringName("createEvent");
releaseEvent = StringName("releaseEvent");
captureEvent = StringName("captureEvent");
addFileAttachment = StringName("addFileAttachment");
addBytesAttachment = StringName("addBytesAttachment");

// Event methods.
eventGetId = StringName("eventGetId");
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/android/android_string_names.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class AndroidStringNames {
_FORCE_INLINE_ static AndroidStringNames *get_singleton() { return singleton; }

// API methods.
StringName addFileAttachment;
StringName setContext;
StringName removeContext;
StringName setTag;
Expand All @@ -41,6 +40,8 @@ class AndroidStringNames {
StringName createEvent;
StringName releaseEvent;
StringName captureEvent;
StringName addFileAttachment;
StringName addBytesAttachment;

// Event methods.
StringName eventGetId;
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/disabled_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class DisabledSDK : public InternalSDK {
virtual Ref<SentryEvent> create_event() override { return memnew(DisabledEvent); }
virtual String capture_event(const Ref<SentryEvent> &p_event) override { return ""; }

virtual void add_attachment(const Ref<SentryAttachment> &p_attachment) override {}

virtual void initialize(const PackedStringArray &p_global_attachments) override {}
};

Expand Down
4 changes: 4 additions & 0 deletions src/sentry/internal_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/variant/string.hpp>

class SentryAttachment;

using namespace godot;

namespace sentry {
Expand All @@ -34,6 +36,8 @@ class InternalSDK {
virtual Ref<SentryEvent> create_event() = 0;
virtual String capture_event(const Ref<SentryEvent> &p_event) = 0;

virtual void add_attachment(const Ref<SentryAttachment> &p_attachment) = 0;

virtual void initialize(const PackedStringArray &p_global_attachments) = 0;

virtual ~InternalSDK() = default;
Expand Down
41 changes: 41 additions & 0 deletions src/sentry/native/native_sdk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include "sentry/native/native_util.h"
#include "sentry/processing/process_event.h"
#include "sentry/util/print.h"
#include "sentry/util/screenshot.h"
#include "sentry_attachment.h"
#include "sentry_options.h"

#include <godot_cpp/classes/file_access.hpp>
Expand Down Expand Up @@ -186,6 +188,45 @@ String NativeSDK::capture_event(const Ref<SentryEvent> &p_event) {
return _uuid_as_string(uuid);
}

void NativeSDK::add_attachment(const Ref<SentryAttachment> &p_attachment) {
ERR_FAIL_COND_MSG(p_attachment.is_null(), "Sentry: Can't add null attachment.");
ERR_FAIL_NULL(ProjectSettings::get_singleton());

sentry_attachment_t *native_attachment = nullptr;

if (!p_attachment->get_path().is_empty()) {
String absolute_path = ProjectSettings::get_singleton()->globalize_path(p_attachment->get_path());
sentry::util::print_debug(vformat("attaching file: %s", absolute_path));

native_attachment = sentry_attach_file(absolute_path.utf8());

ERR_FAIL_NULL_MSG(native_attachment, vformat("Sentry: Failed to attach file: %s", absolute_path));

if (!p_attachment->get_filename().is_empty()) {
sentry_attachment_set_filename(native_attachment, p_attachment->get_filename().utf8());
}

} else {
PackedByteArray bytes = p_attachment->get_bytes();
ERR_FAIL_COND_MSG(bytes.is_empty(), "Sentry: Can't add attachment with empty bytes and no file path.");

sentry::util::print_debug(vformat("attaching bytes with filename: %s", p_attachment->get_filename()));

native_attachment = sentry_attach_bytes(
reinterpret_cast<const char *>(bytes.ptr()),
bytes.size(),
p_attachment->get_filename().utf8());

ERR_FAIL_NULL_MSG(native_attachment, vformat("Sentry: Failed to attach bytes with filename: %s", p_attachment->get_filename()));
}

p_attachment->set_native_attachment(native_attachment);

if (!p_attachment->get_content_type().is_empty()) {
sentry_attachment_set_content_type(native_attachment, p_attachment->get_content_type().utf8());
}
}

void NativeSDK::initialize(const PackedStringArray &p_global_attachments) {
ERR_FAIL_NULL(OS::get_singleton());
ERR_FAIL_NULL(ProjectSettings::get_singleton());
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/native/native_sdk.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class NativeSDK : public InternalSDK {
virtual Ref<SentryEvent> create_event() override;
virtual String capture_event(const Ref<SentryEvent> &p_event) override;

virtual void add_attachment(const Ref<SentryAttachment> &p_attachment) override;

virtual void initialize(const PackedStringArray &p_global_attachments) override;

virtual ~NativeSDK() override;
Expand Down
Loading
Loading