diff --git a/src/Sentry.Maui/Internal/ScreenshotAttachment.cs b/src/Sentry.Maui/Internal/ScreenshotAttachment.cs
index 6d8a2e7cf6..df18fba25f 100644
--- a/src/Sentry.Maui/Internal/ScreenshotAttachment.cs
+++ b/src/Sentry.Maui/Internal/ScreenshotAttachment.cs
@@ -5,22 +5,13 @@ namespace Sentry.Maui.Internal;
internal class ScreenshotAttachment : SentryAttachment
{
public ScreenshotAttachment(SentryMauiOptions options)
- : this(
+ : base(
AttachmentType.Default,
new ScreenshotAttachmentContent(options),
"screenshot.jpg",
"image/jpeg")
{
}
-
- private ScreenshotAttachment(
- AttachmentType type,
- IAttachmentContent content,
- string fileName,
- string? contentType)
- : base(type, content, fileName, contentType)
- {
- }
}
internal class ScreenshotAttachmentContent : IAttachmentContent
diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs
index 227c572797..e70a80526f 100644
--- a/src/Sentry/Protocol/Envelopes/Envelope.cs
+++ b/src/Sentry/Protocol/Envelopes/Envelope.cs
@@ -335,9 +335,12 @@ public static Envelope FromFeedback(
}
///
- /// Creates an envelope that contains a single transaction.
+ /// Creates an envelope that contains a single transaction and optional attachments.
///
- public static Envelope FromTransaction(SentryTransaction transaction)
+ public static Envelope FromTransaction(
+ SentryTransaction transaction,
+ IDiagnosticLogger? logger = null,
+ IReadOnlyCollection? attachments = null)
{
var eventId = transaction.EventId;
var header = CreateHeader(eventId, transaction.DynamicSamplingContext);
@@ -358,6 +361,20 @@ public static Envelope FromTransaction(SentryTransaction transaction)
}
}
+ if (attachments is not null)
+ {
+ foreach (var attachment in attachments)
+ {
+ if (attachment.IsNull())
+ {
+ logger?.LogWarning("Encountered a null attachment. Skipping.");
+ continue;
+ }
+
+ AddEnvelopeItemFromAttachment(items, attachment, logger);
+ }
+ }
+
return new Envelope(eventId, header, items);
}
diff --git a/src/Sentry/SentryAttachment.cs b/src/Sentry/SentryAttachment.cs
index 0249fc589f..c57dc96ed4 100644
--- a/src/Sentry/SentryAttachment.cs
+++ b/src/Sentry/SentryAttachment.cs
@@ -65,6 +65,12 @@ public class SentryAttachment
///
public string? ContentType { get; }
+ ///
+ /// Whether the attachment should be added to transactions.
+ /// Defaults to false.
+ ///
+ public bool AddToTransactions { get; }
+
///
/// Initializes an instance of .
///
@@ -73,10 +79,24 @@ public SentryAttachment(
IAttachmentContent content,
string fileName,
string? contentType)
+ : this(type, content, fileName, contentType, false)
+ {
+ }
+
+ ///
+ /// Initializes an instance of .
+ ///
+ public SentryAttachment(
+ AttachmentType type,
+ IAttachmentContent content,
+ string fileName,
+ string? contentType,
+ bool addToTransactions)
{
Type = type;
Content = content;
FileName = fileName;
ContentType = contentType;
+ AddToTransactions = addToTransactions;
}
}
diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs
index 7bbd403aef..8bded1c002 100644
--- a/src/Sentry/SentryClient.cs
+++ b/src/Sentry/SentryClient.cs
@@ -223,7 +223,10 @@ public void CaptureTransaction(SentryTransaction transaction, Scope? scope, Sent
processedTransaction.Redact();
}
- CaptureEnvelope(Envelope.FromTransaction(processedTransaction));
+ // Keep null entries so the null-attachment guard in Envelope.FromTransaction handles them
+ // (consistent with the event/feedback capture paths); dereferencing them here would throw.
+ var attachments = hint.Attachments.Where(a => a is null || a.AddToTransactions).ToList();
+ CaptureEnvelope(Envelope.FromTransaction(processedTransaction, _options.DiagnosticLogger, attachments));
}
#if NET6_0_OR_GREATER
diff --git a/src/Sentry/ViewHierarchyAttachment.cs b/src/Sentry/ViewHierarchyAttachment.cs
index d71e9e8db5..658ea52305 100644
--- a/src/Sentry/ViewHierarchyAttachment.cs
+++ b/src/Sentry/ViewHierarchyAttachment.cs
@@ -8,8 +8,17 @@ public class ViewHierarchyAttachment : SentryAttachment
///
/// Initializes an instance of .
///
- /// /// The view hierarchy attachment
- public ViewHierarchyAttachment(IAttachmentContent content) :
- base(AttachmentType.ViewHierarchy, content, "view-hierarchy.json", "application/json")
+ /// The view hierarchy attachment
+ public ViewHierarchyAttachment(IAttachmentContent content)
+ : this(content, false)
+ { }
+
+ ///
+ /// Initializes an instance of .
+ ///
+ /// The view hierarchy attachment
+ /// Whether the attachment should be added to transactions.
+ public ViewHierarchyAttachment(IAttachmentContent content, bool addToTransactions)
+ : base(AttachmentType.ViewHierarchy, content, "view-hierarchy.json", "application/json", addToTransactions)
{ }
}
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
index 5bc79de2f1..fe4834dee2 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
@@ -438,6 +438,8 @@ namespace Sentry
public class SentryAttachment
{
public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType) { }
+ public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType, bool addToTransactions) { }
+ public bool AddToTransactions { get; }
public Sentry.IAttachmentContent Content { get; }
public string? ContentType { get; }
public string FileName { get; }
@@ -1415,6 +1417,7 @@ namespace Sentry
public class ViewHierarchyAttachment : Sentry.SentryAttachment
{
public ViewHierarchyAttachment(Sentry.IAttachmentContent content) { }
+ public ViewHierarchyAttachment(Sentry.IAttachmentContent content, bool addToTransactions) { }
}
public abstract class ViewHierarchyNode : Sentry.ISentryJsonSerializable
{
@@ -2017,7 +2020,7 @@ namespace Sentry.Protocol.Envelopes
public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { }
- public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { }
+ public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null) { }
}
public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 5bc79de2f1..fe4834dee2 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -438,6 +438,8 @@ namespace Sentry
public class SentryAttachment
{
public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType) { }
+ public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType, bool addToTransactions) { }
+ public bool AddToTransactions { get; }
public Sentry.IAttachmentContent Content { get; }
public string? ContentType { get; }
public string FileName { get; }
@@ -1415,6 +1417,7 @@ namespace Sentry
public class ViewHierarchyAttachment : Sentry.SentryAttachment
{
public ViewHierarchyAttachment(Sentry.IAttachmentContent content) { }
+ public ViewHierarchyAttachment(Sentry.IAttachmentContent content, bool addToTransactions) { }
}
public abstract class ViewHierarchyNode : Sentry.ISentryJsonSerializable
{
@@ -2017,7 +2020,7 @@ namespace Sentry.Protocol.Envelopes
public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { }
- public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { }
+ public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null) { }
}
public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 5bc79de2f1..fe4834dee2 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -438,6 +438,8 @@ namespace Sentry
public class SentryAttachment
{
public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType) { }
+ public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType, bool addToTransactions) { }
+ public bool AddToTransactions { get; }
public Sentry.IAttachmentContent Content { get; }
public string? ContentType { get; }
public string FileName { get; }
@@ -1415,6 +1417,7 @@ namespace Sentry
public class ViewHierarchyAttachment : Sentry.SentryAttachment
{
public ViewHierarchyAttachment(Sentry.IAttachmentContent content) { }
+ public ViewHierarchyAttachment(Sentry.IAttachmentContent content, bool addToTransactions) { }
}
public abstract class ViewHierarchyNode : Sentry.ISentryJsonSerializable
{
@@ -2017,7 +2020,7 @@ namespace Sentry.Protocol.Envelopes
public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { }
- public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { }
+ public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null) { }
}
public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable
{
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index 6c44d57159..3e8e2017ec 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -426,6 +426,8 @@ namespace Sentry
public class SentryAttachment
{
public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType) { }
+ public SentryAttachment(Sentry.AttachmentType type, Sentry.IAttachmentContent content, string fileName, string? contentType, bool addToTransactions) { }
+ public bool AddToTransactions { get; }
public Sentry.IAttachmentContent Content { get; }
public string? ContentType { get; }
public string FileName { get; }
@@ -1396,6 +1398,7 @@ namespace Sentry
public class ViewHierarchyAttachment : Sentry.SentryAttachment
{
public ViewHierarchyAttachment(Sentry.IAttachmentContent content) { }
+ public ViewHierarchyAttachment(Sentry.IAttachmentContent content, bool addToTransactions) { }
}
public abstract class ViewHierarchyNode : Sentry.ISentryJsonSerializable
{
@@ -1993,7 +1996,7 @@ namespace Sentry.Protocol.Envelopes
public static Sentry.Protocol.Envelopes.Envelope FromEvent(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromFeedback(Sentry.SentryEvent @event, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null, Sentry.SessionUpdate? sessionUpdate = null) { }
public static Sentry.Protocol.Envelopes.Envelope FromSession(Sentry.SessionUpdate sessionUpdate) { }
- public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction) { }
+ public static Sentry.Protocol.Envelopes.Envelope FromTransaction(Sentry.SentryTransaction transaction, Sentry.Extensibility.IDiagnosticLogger? logger = null, System.Collections.Generic.IReadOnlyCollection? attachments = null) { }
}
public sealed class EnvelopeItem : Sentry.Protocol.Envelopes.ISerializable, System.IDisposable
{
diff --git a/test/Sentry.Tests/AttachmentHelper.cs b/test/Sentry.Tests/AttachmentHelper.cs
index 562313fb2a..daa2e7b640 100644
--- a/test/Sentry.Tests/AttachmentHelper.cs
+++ b/test/Sentry.Tests/AttachmentHelper.cs
@@ -2,12 +2,13 @@ namespace Sentry.Tests
{
internal static class AttachmentHelper
{
- internal static SentryAttachment FakeAttachment(string name = "test.txt")
+ internal static SentryAttachment FakeAttachment(string name = "test.txt", bool addToTransactions = false)
=> new(
AttachmentType.Default,
new StreamAttachmentContent(new MemoryStream(new byte[] { 1 })),
name,
- null
+ null,
+ addToTransactions
);
}
}
diff --git a/test/Sentry.Tests/AttachmentTests.cs b/test/Sentry.Tests/AttachmentTests.cs
index 95a3666eca..7e39ba599a 100644
--- a/test/Sentry.Tests/AttachmentTests.cs
+++ b/test/Sentry.Tests/AttachmentTests.cs
@@ -24,6 +24,28 @@ public void GetStream_ReturnsBytesContent()
}
}
+public class ViewHierarchyAttachmentTests
+{
+ [Fact]
+ public void Ctor_DefaultsAddToTransactionsToFalse()
+ {
+ var attachment = new ViewHierarchyAttachment(new ByteAttachmentContent(new byte[] { 1 }));
+ Assert.False(attachment.AddToTransactions);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Ctor_ForwardsAddToTransactions(bool addToTransactions)
+ {
+ var attachment = new ViewHierarchyAttachment(
+ new ByteAttachmentContent(new byte[] { 1 }),
+ addToTransactions);
+
+ Assert.Equal(addToTransactions, attachment.AddToTransactions);
+ }
+}
+
public class FileAttachmentContentTests
{
[Fact]
diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs
index 1835016cf9..732820851e 100644
--- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs
+++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs
@@ -753,6 +753,34 @@ public async Task Roundtrip_WithEvent_WithAttachment_Success()
envelopeRoundtrip.Items[1].TryGetOrRecalculateLength().Should().Be(attachment.Content.GetStream().Length);
}
+ [Fact]
+ public void FromTransaction_WithNullAttachment_SkipsItAndLogsWarning()
+ {
+ // Arrange
+ var tracer = new TransactionTracer(DisabledHub.Instance, "name", "op");
+ var transaction = new SentryTransaction(tracer);
+
+ using var attachmentStream = new MemoryStream(new byte[] { 1, 2, 3 });
+ var validAttachment = new SentryAttachment(
+ AttachmentType.Default,
+ new StreamAttachmentContent(attachmentStream),
+ "file.txt",
+ null);
+
+ var attachments = new List { null!, validAttachment };
+ var logger = new InMemoryDiagnosticLogger();
+
+ // Act
+ using var envelope = Envelope.FromTransaction(transaction, logger, attachments);
+
+ // Assert
+ // Only the transaction item and the single valid attachment - the null is skipped.
+ envelope.Items.Count(item => item.TryGetType() == "attachment").Should().Be(1);
+ logger.Entries.Should().ContainSingle(e =>
+ e.Level == SentryLevel.Warning &&
+ e.Message == "Encountered a null attachment. Skipping.");
+ }
+
[Fact]
public async Task Roundtrip_WithEvent_WithSession_Success()
{
diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs
index 1accc76921..3cb379e9d6 100644
--- a/test/Sentry.Tests/SentryClientTests.cs
+++ b/test/Sentry.Tests/SentryClientTests.cs
@@ -1410,6 +1410,75 @@ public void CaptureTransaction_ScopeContainsAttachments_GetAppliedToHint()
hint.Attachments.Should().Contain(attachments);
}
+ [Fact]
+ public void CaptureTransaction_AttachmentWithAddToTransactionsTrue_IncludedInEnvelope()
+ {
+ // Arrange
+ var transaction = new SentryTransaction("name", "operation")
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now
+ };
+ var attachment = AttachmentHelper.FakeAttachment("include.txt", addToTransactions: true);
+ var scope = new Scope(_fixture.SentryOptions);
+ scope.AddAttachment(attachment);
+ var sut = _fixture.GetSut();
+
+ // Act
+ sut.CaptureTransaction(transaction, scope, null);
+
+ // Assert
+ sut.Worker.Received(1).EnqueueEnvelope(Arg.Is(envelope =>
+ envelope.Items.Count(item => item.TryGetType() == "attachment") == 1));
+ }
+
+ [Fact]
+ public void CaptureTransaction_AttachmentWithAddToTransactionsFalse_ExcludedFromEnvelope()
+ {
+ // Arrange
+ var transaction = new SentryTransaction("name", "operation")
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now
+ };
+ var scope = new Scope(_fixture.SentryOptions);
+ scope.AddAttachment(AttachmentHelper.FakeAttachment("exclude.txt")); // default: AddToTransactions = false
+ var sut = _fixture.GetSut();
+
+ // Act
+ sut.CaptureTransaction(transaction, scope, null);
+
+ // Assert
+ sut.Worker.Received(1).EnqueueEnvelope(Arg.Is(envelope =>
+ envelope.Items.Count(item => item.TryGetType() == "attachment") == 0));
+ }
+
+ [Fact]
+ public void CaptureTransaction_NullAttachmentInHint_DoesNotThrowAndSkipsNull()
+ {
+ // Arrange
+ var transaction = new SentryTransaction("name", "operation")
+ {
+ IsSampled = true,
+ EndTimestamp = DateTimeOffset.Now
+ };
+ var scope = new Scope(_fixture.SentryOptions);
+ var sut = _fixture.GetSut();
+
+ // A null entry in the hint's attachments must not crash transaction capture.
+ var hint = new SentryHint();
+ hint.Attachments.Add(null!);
+ hint.Attachments.Add(AttachmentHelper.FakeAttachment("include.txt", addToTransactions: true));
+
+ // Act
+ var capture = () => sut.CaptureTransaction(transaction, scope, hint);
+
+ // Assert
+ capture.Should().NotThrow();
+ sut.Worker.Received(1).EnqueueEnvelope(Arg.Is(envelope =>
+ envelope.Items.Count(item => item.TryGetType() == "attachment") == 1));
+ }
+
[SkippableFact]
public void CaptureTransaction_UserIsNull_SetsFallbackUserId()
{