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
46 changes: 37 additions & 9 deletions src/Sentry/FileAttachmentContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class FileAttachmentContent : IAttachmentContent
{
private readonly bool _readFileAsynchronously;

private readonly bool _deleteOnClose;

/// <summary>
/// The path to the file to attach.
/// </summary>
Expand All @@ -18,7 +20,16 @@ public class FileAttachmentContent : IAttachmentContent
/// Creates a new instance of <see cref="FileAttachmentContent"/>.
/// </summary>
/// <param name="filePath">The path to the file to attach.</param>
public FileAttachmentContent(string filePath) : this(filePath, true)
public FileAttachmentContent(string filePath) : this(filePath, true, false)
{
}

/// <summary>
/// Creates a new instance of <see cref="FileAttachmentContent"/>.
/// </summary>
/// <param name="filePath">The path to the file to attach.</param>
/// <param name="readFileAsynchronously">Whether to use async file I/O to read the file.</param>
public FileAttachmentContent(string filePath, bool readFileAsynchronously) : this(filePath, readFileAsynchronously, false)
{
}

Expand All @@ -27,18 +38,35 @@ public FileAttachmentContent(string filePath) : this(filePath, true)
/// </summary>
/// <param name="filePath">The path to the file to attach.</param>
/// <param name="readFileAsynchronously">Whether to use async file I/O to read the file.</param>
public FileAttachmentContent(string filePath, bool readFileAsynchronously)
/// <param name="deleteOnClose">Whether to delete the file when it closed.</param>
public FileAttachmentContent(string filePath, bool readFileAsynchronously, bool deleteOnClose)
{
FilePath = filePath;
_readFileAsynchronously = readFileAsynchronously;
_deleteOnClose = deleteOnClose;
}

/// <inheritdoc />
public Stream GetStream() => new FileStream(
FilePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: 4096,
useAsync: _readFileAsynchronously);
public Stream GetStream()
{
var options = FileOptions.None;

if (_readFileAsynchronously)
{
options |= FileOptions.Asynchronous;
}

if (_deleteOnClose)
{
options |= FileOptions.DeleteOnClose;
}

return new FileStream(
FilePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: 4096,
options);
Comment thread
jamescrosswell marked this conversation as resolved.
}
}
2 changes: 1 addition & 1 deletion src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ internal void CaptureHeapDump(string dumpFile)
Level = _options.HeapDumpOptions?.Level ?? SentryLevel.Warning,
};
var hint = new SentryHint(_options);
hint.AddAttachment(dumpFile);
hint.AddAttachment(dumpFile, AttachmentType.HeapDump);
CaptureEvent(evt, CurrentScope, hint);
}
catch (Exception e)
Expand Down
1 change: 1 addition & 0 deletions src/Sentry/Protocol/Envelopes/EnvelopeItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ internal static EnvelopeItem FromAttachment(SentryAttachment attachment, Stream
AttachmentType.UnrealContext => "unreal.context",
AttachmentType.UnrealLogs => "unreal.logs",
AttachmentType.ViewHierarchy => "event.view_hierarchy",
AttachmentType.HeapDump => "event.heapdump",
_ => "event.attachment"
};

Expand Down
9 changes: 8 additions & 1 deletion src/Sentry/SentryAttachment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ public enum AttachmentType
/// <summary>
/// A JSON attachment containing the View Hierarchy
/// </summary>
ViewHierarchy
ViewHierarchy,

/// <summary>
/// A .gcdump file captured when a configured memory threshold is exceeded.
/// Used internally to allow the SDK to clean up the file from disk if it can't be sent to Sentry
/// (e.g. because it exceeds the attachment size limit).
/// </summary>
HeapDump
}

/// <summary>
Expand Down
4 changes: 3 additions & 1 deletion src/Sentry/SentryHint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ public void AddAttachment(
{
if (_options is not null)
{
var deleteOnClose = type == AttachmentType.HeapDump;

_attachments.Add(
new SentryAttachment(
type,
new FileAttachmentContent(filePath, _options.UseAsyncFileIO),
new FileAttachmentContent(filePath, _options.UseAsyncFileIO, deleteOnClose),
Path.GetFileName(filePath),
contentType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry
UnrealContext = 3,
UnrealLogs = 4,
ViewHierarchy = 5,
HeapDump = 6,
}
public class BaggageHeader
{
Expand Down Expand Up @@ -109,6 +110,7 @@ namespace Sentry
{
public FileAttachmentContent(string filePath) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously, bool deleteOnClose) { }
public System.IO.Stream GetStream() { }
}
public static class HasExtraExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry
UnrealContext = 3,
UnrealLogs = 4,
ViewHierarchy = 5,
HeapDump = 6,
}
public class BaggageHeader
{
Expand Down Expand Up @@ -109,6 +110,7 @@ namespace Sentry
{
public FileAttachmentContent(string filePath) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously, bool deleteOnClose) { }
public System.IO.Stream GetStream() { }
}
public static class HasExtraExtensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry
UnrealContext = 3,
UnrealLogs = 4,
ViewHierarchy = 5,
HeapDump = 6,
}
public class BaggageHeader
{
Expand Down Expand Up @@ -109,6 +110,7 @@ namespace Sentry
{
public FileAttachmentContent(string filePath) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously, bool deleteOnClose) { }
public System.IO.Stream GetStream() { }
}
public static class HasExtraExtensions
Expand Down
2 changes: 2 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Sentry
UnrealContext = 3,
UnrealLogs = 4,
ViewHierarchy = 5,
HeapDump = 6,
}
public class BaggageHeader
{
Expand Down Expand Up @@ -99,6 +100,7 @@ namespace Sentry
{
public FileAttachmentContent(string filePath) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously) { }
public FileAttachmentContent(string filePath, bool readFileAsynchronously, bool deleteOnClose) { }
public System.IO.Stream GetStream() { }
}
public static class HasExtraExtensions
Expand Down
43 changes: 43 additions & 0 deletions test/Sentry.Tests/HintTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,48 @@ public void WithAttachments_WithICollection_ReturnsHintWithAttachments()
hint.Attachments.Should().Contain(attachment2);
}

[Fact]
public void AddAttachment_HeapDump_DeletesFileWhenStreamIsClosed()
{
// Arrange
var attachmentPath = Path.Combine(_testDirectory, "dump.gcdump");
File.WriteAllText(attachmentPath, "fake heap dump");

var hint = new SentryHint(new SentryOptions());

// Act
hint.AddAttachment(attachmentPath, AttachmentType.HeapDump);

// Assert
var attachment = Assert.Single(hint.Attachments);
using (attachment.Content.GetStream())
{
File.Exists(attachmentPath).Should().BeTrue("the dump must survive while it is being read");
}

File.Exists(attachmentPath).Should().BeFalse("heap dumps are deleted once the stream is closed");
}

[Fact]
public void AddAttachment_NonHeapDump_LeavesFileInPlaceWhenStreamIsClosed()
{
// Arrange
var attachmentPath = Path.Combine(_testDirectory, "attachment.txt");
File.WriteAllText(attachmentPath, "some user attachment");

var hint = new SentryHint(new SentryOptions());

// Act
hint.AddAttachment(attachmentPath, AttachmentType.Default);

// Assert
var attachment = Assert.Single(hint.Attachments);
using (attachment.Content.GetStream())
{
}

File.Exists(attachmentPath).Should().BeTrue("we must never delete attachments supplied by the user");
}

public void Dispose() => Directory.Delete(_testDirectory, true);
}
38 changes: 38 additions & 0 deletions test/Sentry.Tests/Internals/Http/CachingTransportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,44 @@ public async Task WithAttachment()
}
}

[Fact]
public async Task WithHeapDumpAttachment_DeletesLocalFileButStillSendsIt()
{
// Arrange
string httpContent = null;
var innerTransport = new HttpTransport(_options, new HttpClient(new CallbackHttpClientHandler(async message =>
{
httpContent = await message.Content!.ReadAsStringAsync();
})));

await using var transport = CachingTransport.Create(innerTransport, _options, startWorker: false);

const string dumpContent = "fake heap dump";
var dumpFile = Path.Combine(_cacheDirectory.Path, "dump.gcdump");
File.WriteAllText(dumpFile, dumpContent);

var attachment = new SentryAttachment(
AttachmentType.HeapDump,
new FileAttachmentContent(dumpFile, readFileAsynchronously: true, deleteOnClose: true),
"dump.gcdump",
null);

var envelope = Envelope.FromEvent(new SentryEvent(), attachments: new[] { attachment });

// Act
// The caching transport writes the envelope (including the dump) to the cache directory
// before the envelope is disposed, which is what BackgroundWorker does after each send.
await transport.SendEnvelopeAsync(envelope);
File.Exists(dumpFile).Should().BeTrue("the dump must survive until it has been cached");

envelope.Dispose();
await transport.FlushAsync();

// Assert
File.Exists(dumpFile).Should().BeFalse("the dump should be deleted once the envelope is disposed");
httpContent.Should().Contain(dumpContent, "the cached copy should still be sent to Sentry");
}

[Fact]
public async Task WorksInBackground()
{
Expand Down
Loading
Loading