Skip to content

Add runtime attachments support for Windows/Linux #982

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 13 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -15,6 +15,7 @@
### Features

- Adopt generic variant type in public APIs ([#971](https://github.com/getsentry/sentry-unreal/pull/971))
- Add runtime attachments support for Windows/Linux ([#982](https://github.com/getsentry/sentry-unreal/pull/982))

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "AndroidSentrySubsystem.h"

#include "AndroidSentryAttachment.h"
#include "AndroidSentryBreadcrumb.h"
#include "AndroidSentryEvent.h"
#include "AndroidSentryId.h"
Expand Down Expand Up @@ -127,6 +128,27 @@ void FAndroidSentrySubsystem::ClearBreadcrumbs()
FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::Sentry, "clearBreadcrumbs", "()V");
}

void FAndroidSentrySubsystem::AddAttachment(TSharedPtr<ISentryAttachment> attachment)
{
TSharedPtr<FAndroidSentryAttachment> attachmentAndroid = StaticCastSharedPtr<FAndroidSentryAttachment>(attachment);

FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::SentryBridgeJava, "addAttachment", "(Lio/sentry/Attachment;)V",
attachmentAndroid->GetJObject());
}

void FAndroidSentrySubsystem::RemoveAttachment(TSharedPtr<ISentryAttachment> attachment)
{
TSharedPtr<FAndroidSentryAttachment> attachmentAndroid = StaticCastSharedPtr<FAndroidSentryAttachment>(attachment);

FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::SentryBridgeJava, "removeAttachment", "(Lio/sentry/Attachment;)V",
attachmentAndroid->GetJObject());
}

void FAndroidSentrySubsystem::ClearAttachments()
{
FSentryJavaObjectWrapper::CallStaticMethod<void>(SentryJavaClasses::SentryBridgeJava, "clearAttachments", "()V");
}

TSharedPtr<ISentryId> FAndroidSentrySubsystem::CaptureMessage(const FString& message, ESentryLevel level)
{
auto id = FSentryJavaObjectWrapper::CallStaticObjectMethod<jobject>(SentryJavaClasses::Sentry, "captureMessage", "(Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId;",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class FAndroidSentrySubsystem : public ISentrySubsystem
virtual void AddBreadcrumb(TSharedPtr<ISentryBreadcrumb> breadcrumb) override;
virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap<FString, FSentryVariant>& Data, ESentryLevel Level) override;
virtual void ClearBreadcrumbs() override;
virtual void AddAttachment(TSharedPtr<ISentryAttachment> attachment) override;
virtual void RemoveAttachment(TSharedPtr<ISentryAttachment> attachment) override;
virtual void ClearAttachments() override;
virtual TSharedPtr<ISentryId> CaptureMessage(const FString& message, ESentryLevel level) override;
virtual TSharedPtr<ISentryId> CaptureMessageWithScope(const FString& message, ESentryLevel level, const FSentryScopeDelegate& onConfigureScope) override;
virtual TSharedPtr<ISentryId> CaptureEvent(TSharedPtr<ISentryEvent> event) override;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.HashMap;
import java.util.Map;

import io.sentry.Attachment;
import io.sentry.Breadcrumb;
import io.sentry.Hint;
import io.sentry.IScopes;
Expand Down Expand Up @@ -225,4 +226,17 @@ public static Object getScopeContext(final IScope scope, final String key) {
public static void setScopeExtra(final IScope scope, final String key, final Object values) {
scope.setExtra(key, values.toString());
}

public static void addAttachment(final Attachment attachment) {
Sentry.getGlobalScope().addAttachment(attachment);
}

public static void removeAttachment(final Attachment attachment) {
// Currently, Android SDK doesn't have API allowing to remove individual attachments
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may become a gotcha moment for users. Maybe this should be raised internally. Is it necessary to have the removeAttachment method on a global scope? If so, should it also be included in the underlying SDKs?
Same issue in Godot SDK: getsentry/sentry-godot#205 (comment)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the discrepancies between Native, Android, and Cocoa in terms of attachment API support it looks like we have the following options:

  1. Add clearAttachments method to Native which is already available for Android/Cocoa and expose that for global scope instead of giving user the ability to remove individual attachments
  2. Add removeAttachment to Android/Cocoa to enable removing individual attachments on mobile
  3. Combine 1 and 2 to cover all possible use cases
  4. Leave only addAttachment for global scope without the ability to remove it
  5. Leave things as-is and document the current limitations

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any problems with adding a new sentry-native method to clear attachments in the global scope.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - we can expose only the AddAttachment and ClearAttachments methods for the global scope to align with the mobile SDKs.

This could also simplify the FGenericPlatformSentryAttachment implementation as we’d no longer need to store a sentry_attachment_t reference there.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't clear_attachments() also nuke a screenshot attachment? I wonder if it's going to be an unfortunate side-effect.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem kind of bubbles up. Crashpad doesn't know which attachments are special from sentry-native's perspective. Likewise, sentry-native might not know which attachments are special from a downstream SDKs perspective.

I like the idea of making a distinction between fixed attachments added via options vs. dynamic attachments added via the new API, even though during sentry-native attachment API reviews it was proposed that the old options API could be removed when the missing Crashpad IPC was implemented for macOS. Not sure if it's going to happen anytime soon for macOS using rather complicated Mach ports for IPC, though. 😅

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I’ve worked around this by implementing ClearAttachments for Win/Linux on the Unreal side of things (see commit).

The idea is to store user-defined attachments in an internal vector and remove them one by one using the native sentry_remove_attachment whenever ClearAttachments is called. This way, we avoid accidentally removing any attachments that were added during initialization.

@jpnurmi wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds ok. In principle, sentry_clear_attachments() works the same way with Crashpad to avoid removing the internal __sentry-event and __sentry-breadcrumbN attachments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, not saying we can't or shouldn't improve this later on! 🙂 But I think this is fine for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I believe we're no longer blocked here and can proceed with the merge. Further improvements related to handling attachments may come in subsequent PRs.

}

public static void clearAttachments() {
Sentry.getGlobalScope().clearAttachments();
}

}
22 changes: 22 additions & 0 deletions plugin-dev/Source/Sentry/Private/Apple/AppleSentrySubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "AppleSentrySubsystem.h"

#include "AppleSentryAttachment.h"
#include "AppleSentryBreadcrumb.h"
#include "AppleSentryEvent.h"
#include "AppleSentryId.h"
Expand Down Expand Up @@ -202,6 +203,27 @@ void FAppleSentrySubsystem::ClearBreadcrumbs()
}];
}

void FAppleSentrySubsystem::AddAttachment(TSharedPtr<ISentryAttachment> attachment)
{
TSharedPtr<FAppleSentryAttachment> attachmentApple = StaticCastSharedPtr<FAppleSentryAttachment>(attachment);

[SentrySDK configureScope:^(SentryScope* scope) {
[scope addAttachment:attachmentApple->GetNativeObject()];
}];
}

void FAppleSentrySubsystem::RemoveAttachment(TSharedPtr<ISentryAttachment> attachment)
{
// Currently, Cocoa SDK doesn't have API allowing to remove individual attachments
}

void FAppleSentrySubsystem::ClearAttachments()
{
[SentrySDK configureScope:^(SentryScope* scope) {
[scope clearAttachments];
}];
}

TSharedPtr<ISentryId> FAppleSentrySubsystem::CaptureMessage(const FString& message, ESentryLevel level)
{
FSentryScopeDelegate onConfigureScope;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class FAppleSentrySubsystem : public ISentrySubsystem
virtual void AddBreadcrumb(TSharedPtr<ISentryBreadcrumb> breadcrumb) override;
virtual void AddBreadcrumbWithParams(const FString& Message, const FString& Category, const FString& Type, const TMap<FString, FSentryVariant>& Data, ESentryLevel Level) override;
virtual void ClearBreadcrumbs() override;
virtual void AddAttachment(TSharedPtr<ISentryAttachment> attachment) override;
virtual void RemoveAttachment(TSharedPtr<ISentryAttachment> attachment) override;
virtual void ClearAttachments() override;
virtual TSharedPtr<ISentryId> CaptureMessage(const FString& message, ESentryLevel level) override;
virtual TSharedPtr<ISentryId> CaptureMessageWithScope(const FString& message, ESentryLevel level, const FSentryScopeDelegate& onConfigureScope) override;
virtual TSharedPtr<ISentryId> CaptureEvent(TSharedPtr<ISentryEvent> event) override;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2025 Sentry. All Rights Reserved.

#include "GenericPlatformSentryAttachment.h"

#if USE_SENTRY_NATIVE

FGenericPlatformSentryAttachment::FGenericPlatformSentryAttachment(const TArray<uint8>& data, const FString& filename, const FString& contentType)
: Data(data), Filename(filename), ContentType(contentType), Attachment(nullptr)
{
}

FGenericPlatformSentryAttachment::FGenericPlatformSentryAttachment(const FString& path, const FString& filename, const FString& contentType)
: Path(path), Filename(filename), ContentType(contentType), Attachment(nullptr)
{
}

FGenericPlatformSentryAttachment::~FGenericPlatformSentryAttachment()
{
// Put custom destructor logic here if needed
}

void FGenericPlatformSentryAttachment::SetNativeObject(sentry_attachment_t* attachment)
{
Attachment = attachment;
}

sentry_attachment_t* FGenericPlatformSentryAttachment::GetNativeObject()
{
return Attachment;
}

TArray<uint8> FGenericPlatformSentryAttachment::GetData() const
{
return Data;
}

FString FGenericPlatformSentryAttachment::GetPath() const
{
return Path;
}

FString FGenericPlatformSentryAttachment::GetFilename() const
{
return Filename;
}

FString FGenericPlatformSentryAttachment::GetContentType() const
{
return ContentType;
}

const TArray<uint8>& FGenericPlatformSentryAttachment::GetDataByRef() const
{
return Data;
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) 2025 Sentry. All Rights Reserved.

#pragma once

#include "Convenience/GenericPlatformSentryInclude.h"

#include "Interface/SentryAttachmentInterface.h"

#if USE_SENTRY_NATIVE

class FGenericPlatformSentryAttachment : public ISentryAttachment
{
public:
FGenericPlatformSentryAttachment(const TArray<uint8>& data, const FString& filename, const FString& contentType);
FGenericPlatformSentryAttachment(const FString& path, const FString& filename, const FString& contentType);
virtual ~FGenericPlatformSentryAttachment() override;

void SetNativeObject(sentry_attachment_t* attachment);
sentry_attachment_t* GetNativeObject();

virtual TArray<uint8> GetData() const override;
virtual FString GetPath() const override;
virtual FString GetFilename() const override;
virtual FString GetContentType() const override;

const TArray<uint8>& GetDataByRef() const;

private:
TArray<uint8> Data;
FString Path;
FString Filename;
FString ContentType;

sentry_attachment_t* Attachment;
};

typedef FGenericPlatformSentryAttachment FPlatformSentryAttachment;

#endif
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025 Sentry. All Rights Reserved.

#include "GenericPlatformSentryScope.h"
#include "GenericPlatformSentryAttachment.h"
#include "GenericPlatformSentryBreadcrumb.h"
#include "GenericPlatformSentryEvent.h"

Expand Down Expand Up @@ -39,12 +40,12 @@ void FGenericPlatformSentryScope::ClearBreadcrumbs()

void FGenericPlatformSentryScope::AddAttachment(TSharedPtr<ISentryAttachment> attachment)
{
// Not available for generic platform
Attachments.Add(StaticCastSharedPtr<FGenericPlatformSentryAttachment>(attachment));
}

void FGenericPlatformSentryScope::ClearAttachments()
{
// Not available for generic platform
Attachments.Empty();
}

void FGenericPlatformSentryScope::SetTag(const FString& key, const FString& value)
Expand Down Expand Up @@ -195,6 +196,18 @@ void FGenericPlatformSentryScope::Apply(sentry_scope_t* scope)
sentry_scope_add_breadcrumb(scope, nativeBreadcrumb);
}

for (auto& Attachment : Attachments)
{
if (!Attachment->GetPath().IsEmpty())
{
AddFileAttachment(Attachment, scope);
}
else
{
AddByteAttachment(Attachment, scope);
}
}

if (Fingerprint.Num() > 0)
{
sentry_scope_set_fingerprints(scope, FGenericPlatformSentryConverters::StringArrayToNative(Fingerprint));
Expand All @@ -218,4 +231,31 @@ void FGenericPlatformSentryScope::Apply(sentry_scope_t* scope)
sentry_scope_set_level(scope, FGenericPlatformSentryConverters::SentryLevelToNative(Level));
}

void FGenericPlatformSentryScope::AddFileAttachment(TSharedPtr<FGenericPlatformSentryAttachment> attachment, sentry_scope_t* scope)
{
sentry_attachment_t* nativeAttachment =
sentry_scope_attach_file(scope, TCHAR_TO_UTF8(*attachment->GetPath()));

if (!attachment->GetFilename().IsEmpty())
sentry_attachment_set_filename(nativeAttachment, TCHAR_TO_UTF8(*attachment->GetFilename()));

if (!attachment->GetContentType().IsEmpty())
sentry_attachment_set_content_type(nativeAttachment, TCHAR_TO_UTF8(*attachment->GetContentType()));

attachment->SetNativeObject(nativeAttachment);
}

void FGenericPlatformSentryScope::AddByteAttachment(TSharedPtr<FGenericPlatformSentryAttachment> attachment, sentry_scope_t* scope)
{
const TArray<uint8>& byteBuf = attachment->GetDataByRef();

sentry_attachment_t* nativeAttachment =
sentry_scope_attach_bytes(scope, reinterpret_cast<const char*>(byteBuf.GetData()), byteBuf.Num(), TCHAR_TO_UTF8(*attachment->GetFilename()));

if (!attachment->GetContentType().IsEmpty())
sentry_attachment_set_content_type(nativeAttachment, TCHAR_TO_UTF8(*attachment->GetContentType()));

attachment->SetNativeObject(nativeAttachment);
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#if USE_SENTRY_NATIVE

class FGenericPlatformSentryAttachment;
class FGenericPlatformSentryBreadcrumb;
class FGenericPlatformSentryEvent;

Expand Down Expand Up @@ -46,6 +47,10 @@ class FGenericPlatformSentryScope : public ISentryScope

void Apply(sentry_scope_t* scope);

protected:
virtual void AddFileAttachment(TSharedPtr<FGenericPlatformSentryAttachment> attachment, sentry_scope_t* scope);
virtual void AddByteAttachment(TSharedPtr<FGenericPlatformSentryAttachment> attachment, sentry_scope_t* scope);

private:
FString Dist;
FString Environment;
Expand All @@ -59,9 +64,13 @@ class FGenericPlatformSentryScope : public ISentryScope

TRingBuffer<TSharedPtr<FGenericPlatformSentryBreadcrumb>> Breadcrumbs;

TArray<TSharedPtr<FGenericPlatformSentryAttachment>> Attachments;

ESentryLevel Level;
};

#if !PLATFORM_MICROSOFT
typedef FGenericPlatformSentryScope FPlatformSentryScope;
#endif

#endif
Loading