Skip to content
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
23 changes: 23 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Sentry.Extensibility;
using Sentry.Infrastructure;
using Sentry.Integrations;
using Sentry.Internal.Extensions;
using Sentry.Protocol.Envelopes;
using Sentry.Protocol.Metrics;

Expand Down Expand Up @@ -757,6 +758,28 @@ public SentryId CaptureCheckIn(
return SentryId.Empty;
}

// Internal capture method that allows the Unity SDK to send attachments after an already captured event.
// Kept internal as the preferred way of adding attachments is either on the scope or directly on the event.
// See https://develop.sentry.dev/sdk/data-model/envelope-items/#attachment
internal bool CaptureAttachment(SentryId eventId, SentryAttachment attachment)
Comment thread
bitsandfoxes marked this conversation as resolved.
Comment thread
Flash0ver marked this conversation as resolved.
{
if (!IsEnabled || eventId == SentryId.Empty || attachment.IsNull())
{
return false;
}

try
{
var envelope = Envelope.FromAttachment(eventId, attachment, _options.DiagnosticLogger);
return CaptureEnvelope(envelope);
}
catch (Exception e)
{
_options.LogError(e, "Failure to capture attachment");
return false;
}
}

public async Task FlushAsync(TimeSpan timeout)
{
try
Expand Down
6 changes: 6 additions & 0 deletions src/Sentry/Protocol/Envelopes/Envelope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,12 @@ internal static Envelope FromClientReport(ClientReport clientReport)
return new Envelope(header, items);
}

/// <summary>
/// Creates an envelope that contains only an attachment for an existing event.
/// </summary>
internal static Envelope FromAttachment(SentryId eventId, SentryAttachment attachment, IDiagnosticLogger? logger = null) =>
new(eventId, CreateHeader(eventId), [EnvelopeItem.FromAttachment(attachment)]);

private static async Task<IReadOnlyDictionary<string, object?>> DeserializeHeaderAsync(
Stream stream,
CancellationToken cancellationToken = default)
Expand Down
10 changes: 10 additions & 0 deletions test/Sentry.Testing/NullAttachmentContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sentry.Testing;

internal sealed class NullAttachmentContent : IAttachmentContent
Comment thread
bitsandfoxes marked this conversation as resolved.
{
public static NullAttachmentContent Instance { get; } = new();

public Stream GetStream() => Stream.Null;

private NullAttachmentContent() { }
}
61 changes: 61 additions & 0 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,67 @@ public void CaptureFeedback_ConfigureScope_ScopeApplied(bool enabled)
_fixture.Client.Received(enabled ? 1 : 0).CaptureFeedback(Arg.Any<SentryFeedback>(), Arg.Is<Scope>(s => s.Tags["foo"] == "bar"), Arg.Any<SentryHint>());
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void CaptureAttachment_HubEnabled(bool enabled)
{
// Arrange
var hub = _fixture.GetSut();
if (!enabled)
{
hub.Dispose();
}

_fixture.Client.CaptureEnvelope(Arg.Any<Envelope>()).Returns(true);
Comment thread
bitsandfoxes marked this conversation as resolved.

var eventId = SentryId.Create();
var attachment = new SentryAttachment(
AttachmentType.Default,
new ByteAttachmentContent("test content"u8.ToArray()),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: could/should we add an internal constructor, that takes a ReadOnlySpan<byte>, so that we can omit the .ToArray() at the call sites?

internal ByteAttachmentContent(ReadOnlySpan<byte> bytes) => _bytes = bytes.ToArray();

Or is that overkill, because we only have 2 usages that would be shortened.

"test.txt",
"text/plain");

// Act
var result = hub.CaptureAttachment(eventId, attachment);

// Assert
result.Should().Be(enabled);
_fixture.Client.Received(enabled ? 1 : 0).CaptureEnvelope(Arg.Any<Envelope>());
}

[Fact]
public void CaptureAttachment_SentryIdEmpty_ReturnsFalse()
{
// Arrange
var hub = _fixture.GetSut();

var eventId = SentryId.Empty;
var attachment = new SentryAttachment(AttachmentType.Default, NullAttachmentContent.Instance, "test.txt", "text/plain");

// Act
var result = hub.CaptureAttachment(eventId, attachment);

// Assert
result.Should().Be(false);
_fixture.Client.DidNotReceive().CaptureEnvelope(Arg.Any<Envelope>());
}

[Fact]
public void CaptureAttachment_AttachmentNull_ReturnsFalse()
{
// Arrange
var hub = _fixture.GetSut();
var eventId = SentryId.Create();

// Act
var result = hub.CaptureAttachment(eventId, null!);

// Assert
result.Should().Be(false);
_fixture.Client.DidNotReceive().CaptureEnvelope(Arg.Any<Envelope>());
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
22 changes: 22 additions & 0 deletions test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,4 +1090,26 @@ public async Task Serialization_RoundTrip_RecalculatesLengthHeader()
Assert.Contains("""{"foo":"2020-01-01T00:00:00+01:00"}""", serialized1);
Assert.Contains("""{"foo":"2020-01-01T00:00:00\u002B01:00"}""", serialized2);
}

[Fact]
public void FromAttachment_ValidAttachment_CreatesEnvelope()
{
// Arrange
var eventId = SentryId.Create();
var attachment = new SentryAttachment(
AttachmentType.Default,
new ByteAttachmentContent("test content"u8.ToArray()),
"test.txt",
"text/plain");

// Act
using var envelope = Envelope.FromAttachment(eventId, attachment);

// Assert
envelope.TryGetEventId().Should().Be(eventId);
envelope.Items.Should().HaveCount(1);
Comment thread
Flash0ver marked this conversation as resolved.
envelope.Items[0].Header["type"].Should().Be("attachment");
envelope.Items[0].Header["filename"].Should().Be("test.txt");
envelope.Items[0].Header["content_type"].Should().Be("text/plain");
}
}
Loading