diff --git a/src/Sentry/ByteAttachmentContent.cs b/src/Sentry/ByteAttachmentContent.cs
index 56df2bdfda..31ec59e8b9 100644
--- a/src/Sentry/ByteAttachmentContent.cs
+++ b/src/Sentry/ByteAttachmentContent.cs
@@ -5,13 +5,16 @@ namespace Sentry;
///
public class ByteAttachmentContent : IAttachmentContent
{
- private readonly byte[] _bytes;
+ ///
+ /// The raw bytes of the attachment.
+ ///
+ internal byte[] Bytes { get; }
///
/// Creates a new instance of .
///
- public ByteAttachmentContent(byte[] bytes) => _bytes = bytes;
+ public ByteAttachmentContent(byte[] bytes) => Bytes = bytes;
///
- public Stream GetStream() => new MemoryStream(_bytes);
+ public Stream GetStream() => new MemoryStream(Bytes);
}
diff --git a/src/Sentry/FileAttachmentContent.cs b/src/Sentry/FileAttachmentContent.cs
index 97b60f6de5..ed3bb4203b 100644
--- a/src/Sentry/FileAttachmentContent.cs
+++ b/src/Sentry/FileAttachmentContent.cs
@@ -7,9 +7,13 @@ namespace Sentry;
///
public class FileAttachmentContent : IAttachmentContent
{
- private readonly string _filePath;
private readonly bool _readFileAsynchronously;
+ ///
+ /// The path to the file to attach.
+ ///
+ internal string FilePath { get; }
+
///
/// Creates a new instance of .
///
@@ -25,13 +29,13 @@ public FileAttachmentContent(string filePath) : this(filePath, true)
/// Whether to use async file I/O to read the file.
public FileAttachmentContent(string filePath, bool readFileAsynchronously)
{
- _filePath = filePath;
+ FilePath = filePath;
_readFileAsynchronously = readFileAsynchronously;
}
///
public Stream GetStream() => new FileStream(
- _filePath,
+ FilePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
diff --git a/src/Sentry/IScopeObserver.cs b/src/Sentry/IScopeObserver.cs
index 2e8c8ec6ab..870a160a30 100644
--- a/src/Sentry/IScopeObserver.cs
+++ b/src/Sentry/IScopeObserver.cs
@@ -34,4 +34,14 @@ public interface IScopeObserver
/// Sets the current trace
///
public void SetTrace(SentryId traceId, SpanId parentSpanId);
+
+ ///
+ /// Adds an attachment.
+ ///
+ public void AddAttachment(SentryAttachment attachment);
+
+ ///
+ /// Clears all attachments.
+ ///
+ public void ClearAttachments();
}
diff --git a/src/Sentry/Internal/ScopeObserver.cs b/src/Sentry/Internal/ScopeObserver.cs
index feb1411747..28f4676f84 100644
--- a/src/Sentry/Internal/ScopeObserver.cs
+++ b/src/Sentry/Internal/ScopeObserver.cs
@@ -94,4 +94,22 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)
}
public abstract void SetTraceImpl(SentryId traceId, SpanId parentSpanId);
+
+ public void AddAttachment(SentryAttachment attachment)
+ {
+ _options.DiagnosticLogger?.Log(SentryLevel.Debug,
+ "{0} Scope Sync - Adding attachment '{1}'", null, _name, attachment.FileName);
+ AddAttachmentImpl(attachment);
+ }
+
+ public abstract void AddAttachmentImpl(SentryAttachment attachment);
+
+ public void ClearAttachments()
+ {
+ _options.DiagnosticLogger?.Log(
+ SentryLevel.Debug, "{0} Scope Sync - Clearing attachments", null, _name);
+ ClearAttachmentsImpl();
+ }
+
+ public abstract void ClearAttachmentsImpl();
}
diff --git a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs
index c2a272061f..a05f09c575 100644
--- a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs
+++ b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs
@@ -104,4 +104,28 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)
{
// TODO: This requires sentry-java 8.4.0
}
+
+ public void AddAttachment(SentryAttachment attachment)
+ {
+ try
+ {
+ // TODO: Missing corresponding functionality on the Android SDK
+ }
+ finally
+ {
+ _innerObserver?.AddAttachment(attachment);
+ }
+ }
+
+ public void ClearAttachments()
+ {
+ try
+ {
+ // TODO: Missing corresponding functionality on the Android SDK
+ }
+ finally
+ {
+ _innerObserver?.ClearAttachments();
+ }
+ }
}
diff --git a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs
index d4e7def7a8..d7a790d8ff 100644
--- a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs
+++ b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs
@@ -112,4 +112,28 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)
{
// TODO: Missing corresponding functionality on the Cocoa SDK
}
+
+ public void AddAttachment(SentryAttachment attachment)
+ {
+ try
+ {
+ // TODO: Missing corresponding functionality on the Cocoa SDK
+ }
+ finally
+ {
+ _innerObserver?.AddAttachment(attachment);
+ }
+ }
+
+ public void ClearAttachments()
+ {
+ try
+ {
+ // TODO: Missing corresponding functionality on the Cocoa SDK
+ }
+ finally
+ {
+ _innerObserver?.ClearAttachments();
+ }
+ }
}
diff --git a/src/Sentry/Platforms/Native/NativeScopeObserver.cs b/src/Sentry/Platforms/Native/NativeScopeObserver.cs
index b278bf1e83..f9034df998 100644
--- a/src/Sentry/Platforms/Native/NativeScopeObserver.cs
+++ b/src/Sentry/Platforms/Native/NativeScopeObserver.cs
@@ -43,6 +43,16 @@ public override void SetUserImpl(SentryUser user)
public override void SetTraceImpl(SentryId traceId, SpanId parentSpanId) =>
C.sentry_set_trace(traceId.ToString(), parentSpanId.ToString());
+ public override void AddAttachmentImpl(SentryAttachment attachment)
+ {
+ // TODO: Missing corresponding functionality on the Native SDK
+ }
+
+ public override void ClearAttachmentsImpl()
+ {
+ // TODO: Missing corresponding functionality on the Native SDK
+ }
+
private static string GetTimestamp(DateTimeOffset timestamp) =>
// "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly.
// https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip
diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs
index 1df554b8ed..8a11e1f975 100644
--- a/src/Sentry/Scope.cs
+++ b/src/Sentry/Scope.cs
@@ -390,7 +390,14 @@ public void UnsetTag(string key)
///
/// Adds an attachment.
///
- public void AddAttachment(SentryAttachment attachment) => _attachments.Add(attachment);
+ public void AddAttachment(SentryAttachment attachment)
+ {
+ _attachments.Add(attachment);
+ if (Options.EnableScopeSync)
+ {
+ Options.ScopeObserver?.AddAttachment(attachment);
+ }
+ }
internal void SetPropagationContext(SentryPropagationContext propagationContext)
{
@@ -433,6 +440,10 @@ public void ClearAttachments()
#else
_attachments.Clear();
#endif
+ if (Options.EnableScopeSync)
+ {
+ Options.ScopeObserver?.ClearAttachments();
+ }
}
///
@@ -535,7 +546,8 @@ public void Apply(Scope other)
foreach (var attachment in Attachments)
{
- other.AddAttachment(attachment);
+ // Set the attachment directly to avoid triggering a scope sync
+ other._attachments.Add(attachment);
}
}
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
index 64869d4587..16cf15b603 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
@@ -217,7 +217,9 @@ namespace Sentry
}
public interface IScopeObserver
{
+ void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
+ void ClearAttachments();
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 64869d4587..16cf15b603 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -217,7 +217,9 @@ namespace Sentry
}
public interface IScopeObserver
{
+ void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
+ void ClearAttachments();
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 64869d4587..16cf15b603 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -217,7 +217,9 @@ namespace Sentry
}
public interface IScopeObserver
{
+ void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
+ void ClearAttachments();
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index fbf517dd1e..93df540504 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -204,7 +204,9 @@ namespace Sentry
}
public interface IScopeObserver
{
+ void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
+ void ClearAttachments();
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
diff --git a/test/Sentry.Tests/AttachmentTests.cs b/test/Sentry.Tests/AttachmentTests.cs
index 632238d25b..95a3666eca 100644
--- a/test/Sentry.Tests/AttachmentTests.cs
+++ b/test/Sentry.Tests/AttachmentTests.cs
@@ -1,7 +1,38 @@
namespace Sentry.Tests;
+public class ByteAttachmentContentTests
+{
+ [Fact]
+ public void Bytes_ReturnsConstructorValue()
+ {
+ var data = new byte[] { 1, 2, 3 };
+ var content = new ByteAttachmentContent(data);
+ Assert.Same(data, content.Bytes);
+ }
+
+ [Fact]
+ public void GetStream_ReturnsBytesContent()
+ {
+ var data = new byte[] { 10, 20, 30 };
+ var content = new ByteAttachmentContent(data);
+
+ using var stream = content.GetStream();
+ using var ms = new MemoryStream();
+ stream.CopyTo(ms);
+
+ Assert.Equal(data, ms.ToArray());
+ }
+}
+
public class FileAttachmentContentTests
{
+ [Fact]
+ public void FilePath_ReturnsConstructorValue()
+ {
+ var attachment = new FileAttachmentContent("/some/path/file.txt");
+ Assert.Equal("/some/path/file.txt", attachment.FilePath);
+ }
+
[Fact]
public void DoesNotLock()
{
diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs
index 0bb65dc8ec..ccb1b277bf 100644
--- a/test/Sentry.Tests/ScopeTests.cs
+++ b/test/Sentry.Tests/ScopeTests.cs
@@ -737,6 +737,129 @@ public void SetTag_NullValue_DoesNotThrowArgumentNullException()
Assert.Null(exception);
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void AddAttachment_ObserverExist_ObserverNotifiedIfEnabled(bool enableScopeSync)
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var scope = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = enableScopeSync
+ });
+ var attachment = new SentryAttachment(default, default, default, "test.txt");
+ var expectedCount = enableScopeSync ? 1 : 0;
+
+ // Act
+ scope.AddAttachment(attachment);
+
+ // Assert
+ observer.Received(expectedCount).AddAttachment(Arg.Is(attachment));
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ClearAttachments_ObserverExist_ObserverNotifiedIfEnabled(bool enableScopeSync)
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var scope = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = enableScopeSync
+ });
+ scope.AddAttachment(new SentryAttachment(default, default, default, "test.txt"));
+ observer.ClearReceivedCalls();
+ var expectedCount = enableScopeSync ? 1 : 0;
+
+ // Act
+ scope.ClearAttachments();
+
+ // Assert
+ observer.Received(expectedCount).ClearAttachments();
+ }
+
+ [Fact]
+ public void Apply_Attachments_DoesNotNotifyObserver()
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var source = new Scope(new SentryOptions());
+ source.AddAttachment(new SentryAttachment(default, default, default, "test.txt"));
+
+ var target = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = true
+ });
+ observer.ClearReceivedCalls();
+
+ // Act
+ source.Apply(target);
+
+ // Assert
+ observer.DidNotReceive().AddAttachment(Arg.Any());
+ }
+
+ [Fact]
+ public void AddAttachment_ByteOverload_NotifiesObserver()
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var scope = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = true
+ });
+
+ // Act
+ scope.AddAttachment(new byte[] { 1, 2, 3 }, "bytes.bin");
+
+ // Assert
+ observer.Received(1).AddAttachment(Arg.Is(a => a.FileName == "bytes.bin"));
+ }
+
+ [Fact]
+ public void AddAttachment_StreamOverload_NotifiesObserver()
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var scope = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = true
+ });
+
+ // Act
+ scope.AddAttachment(new MemoryStream(new byte[] { 1, 2, 3 }), "stream.bin");
+
+ // Assert
+ observer.Received(1).AddAttachment(Arg.Is(a => a.FileName == "stream.bin"));
+ }
+
+ [Fact]
+ public void Clear_ObserverExist_NotifiesClearAttachments()
+ {
+ // Arrange
+ var observer = Substitute.For();
+ var scope = new Scope(new SentryOptions
+ {
+ ScopeObserver = observer,
+ EnableScopeSync = true
+ });
+ scope.AddAttachment(new SentryAttachment(default, default, default, "test.txt"));
+ observer.ClearReceivedCalls();
+
+ // Act
+ scope.Clear();
+
+ // Assert
+ observer.Received(1).ClearAttachments();
+ }
+
[Theory]
[InlineData(true)]
[InlineData(false)]