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)]