From adc29fe5863463d47f8de09ad6d1be55177e65bb Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 17:09:44 +0100 Subject: [PATCH 1/9] Added BeforeSendScreenshot callback --- src/Sentry.Unity/ScreenshotEventProcessor.cs | 45 +++- src/Sentry.Unity/SentryScreenshot.cs | 22 +- src/Sentry.Unity/SentryUnityOptions.cs | 19 +- src/Sentry.Unity/SentryUnitySdk.cs | 16 +- .../ScreenshotEventProcessorTests.cs | 237 +++++++++++++++++- .../SentryScreenshotTests.cs | 10 +- 6 files changed, 300 insertions(+), 49 deletions(-) diff --git a/src/Sentry.Unity/ScreenshotEventProcessor.cs b/src/Sentry.Unity/ScreenshotEventProcessor.cs index 0e1150c55..154cab4fd 100644 --- a/src/Sentry.Unity/ScreenshotEventProcessor.cs +++ b/src/Sentry.Unity/ScreenshotEventProcessor.cs @@ -27,13 +27,13 @@ public SentryEvent Process(SentryEvent @event) if (Interlocked.CompareExchange(ref _isCapturingScreenshot, 1, 0) == 0) { _options.LogDebug("Starting coroutine to capture a screenshot."); - _sentryMonoBehaviour.QueueCoroutine(CaptureScreenshotCoroutine(@event.EventId)); + _sentryMonoBehaviour.QueueCoroutine(CaptureScreenshotCoroutine(@event)); } return @event; } - internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) + internal IEnumerator CaptureScreenshotCoroutine(SentryEvent @event) { _options.LogDebug("Screenshot capture triggered. Waiting for End of Frame."); @@ -41,6 +41,7 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) // See https://docs.unity3d.com/6000.1/Documentation/ScriptReference/WaitForEndOfFrame.html yield return WaitForEndOfFrame(); + Texture2D? screenshot = null; try { if (_options.BeforeCaptureScreenshotInternal?.Invoke() is false) @@ -48,10 +49,31 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) yield break; } - var screenshotBytes = CaptureScreenshot(_options); - if (screenshotBytes.Length == 0) + screenshot = CreateNewScreenshotTexture2D(_options); + + if (_options.BeforeSendScreenshotInternal != null) + { + var modifiedScreenshot = _options.BeforeSendScreenshotInternal(screenshot, @event); + + if (modifiedScreenshot == null) + { + _options.LogInfo("Screenshot discarded by BeforeSendScreenshot callback."); + yield break; + } + + // Clean up - If the user returned a new texture object and did not modify the passed in one + if (modifiedScreenshot != screenshot) + { + _options.LogDebug("Applying modified screenshot."); + UnityEngine.Object.Destroy(screenshot); + screenshot = modifiedScreenshot; + } + } + + var screenshotBytes = screenshot.EncodeToJPG(_options.ScreenshotCompression); + if (screenshotBytes is null || screenshotBytes.Length == 0) { - _options.LogWarning("Screenshot capture returned empty data for event {0}", eventId); + _options.LogWarning("Screenshot capture returned empty data for event {0}", @event.EventId); yield break; } @@ -61,9 +83,9 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) "screenshot.jpg", "image/jpeg"); - _options.LogDebug("Screenshot captured for event {0}", eventId); + _options.LogDebug("Screenshot captured for event {0}", @event.EventId); - CaptureAttachment(eventId, attachment); + CaptureAttachment(@event.EventId, attachment); } catch (Exception e) { @@ -72,11 +94,16 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryId eventId) finally { Interlocked.Exchange(ref _isCapturingScreenshot, 0); + + if (screenshot != null) + { + UnityEngine.Object.Destroy(screenshot); + } } } - internal virtual byte[] CaptureScreenshot(SentryUnityOptions options) - => SentryScreenshot.Capture(options); + internal virtual Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions options) + => SentryScreenshot.CreateNewScreenshotTexture2D(options); internal virtual void CaptureAttachment(SentryId eventId, SentryAttachment attachment) => (Sentry.SentrySdk.CurrentHub as Hub)?.CaptureAttachment(eventId, attachment); diff --git a/src/Sentry.Unity/SentryScreenshot.cs b/src/Sentry.Unity/SentryScreenshot.cs index 718fb33bc..3e9c163aa 100644 --- a/src/Sentry.Unity/SentryScreenshot.cs +++ b/src/Sentry.Unity/SentryScreenshot.cs @@ -1,3 +1,4 @@ +using Sentry.Extensibility; using UnityEngine; namespace Sentry.Unity; @@ -15,11 +16,11 @@ internal static int GetTargetResolution(ScreenshotQuality quality) }; } - public static byte[] Capture(SentryUnityOptions options) => - Capture(options, Screen.width, Screen.height); + public static Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions options) => + CreateNewScreenshotTexture2D(options, Screen.width, Screen.height); // For testing - internal static byte[] Capture(SentryUnityOptions options, int width, int height) + internal static Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions options, int width, int height) { // Make sure the screenshot size does not exceed the target size by scaling the image while conserving the // original ratio based on which, width or height, is the smaller @@ -36,7 +37,6 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height } } - Texture2D? screenshot = null; RenderTexture? renderTextureFull = null; RenderTexture? renderTextureResized = null; var previousRenderTexture = RenderTexture.active; @@ -44,7 +44,7 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height try { // Captures the current screenshot synchronously. - screenshot = new Texture2D(width, height, TextureFormat.RGB24, false); + var screenshot = new Texture2D(width, height, TextureFormat.RGB24, false); renderTextureFull = RenderTexture.GetTemporary(Screen.width, Screen.height); ScreenCapture.CaptureScreenshotIntoRenderTexture(renderTextureFull); renderTextureResized = RenderTexture.GetTemporary(width, height); @@ -66,12 +66,9 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply(); - var bytes = screenshot.EncodeToJPG(options.ScreenshotCompression); + options.LogDebug("Screenshot captured at {0}x{1}: {2} bytes", width, height); - options.DiagnosticLogger?.Log(SentryLevel.Debug, - "Screenshot captured at {0}x{1}: {2} bytes", null, width, height, bytes.Length); - - return bytes; + return screenshot; } finally { @@ -86,11 +83,6 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height { RenderTexture.ReleaseTemporary(renderTextureResized); } - - if (screenshot) - { - Object.Destroy(screenshot); - } } } } diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 899398f11..8304e6529 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -252,9 +252,22 @@ public sealed class SentryUnityOptions : SentryOptions /// public new StackTraceMode StackTraceMode { get; private set; } - private Func? _beforeCaptureScreenshot; + internal Func? BeforeCaptureScreenshotInternal { get; private set; } - internal Func? BeforeCaptureScreenshotInternal => _beforeCaptureScreenshot; + internal Func? BeforeSendScreenshotInternal { get; private set; } + + /// + /// Configures a callback to modify or discard screenshots before they are sent. + /// + /// + /// This callback receives the captured screenshot as a Texture2D before JPEG compression. + /// You can modify the texture (blur areas, redact PII, etc.) and return it, or return null to discard. + /// + /// The callback function to invoke before sending screenshots. + public void SetBeforeSendScreenshot(Func beforeSendScreenshot) + { + BeforeSendScreenshotInternal = beforeSendScreenshot; + } /// /// Configures a callback function to be invoked before capturing and attaching a screenshot to an event. @@ -265,7 +278,7 @@ public sealed class SentryUnityOptions : SentryOptions /// public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) { - _beforeCaptureScreenshot = beforeAttachScreenshot; + BeforeCaptureScreenshotInternal = beforeAttachScreenshot; } private Func? _beforeCaptureViewHierarchy; diff --git a/src/Sentry.Unity/SentryUnitySdk.cs b/src/Sentry.Unity/SentryUnitySdk.cs index 098d9b862..8fa24fc99 100644 --- a/src/Sentry.Unity/SentryUnitySdk.cs +++ b/src/Sentry.Unity/SentryUnitySdk.cs @@ -121,14 +121,20 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad return; } - var hint = addScreenshot - ? SentryHint.WithAttachments( + SentryHint? hint = null; + if (addScreenshot) + { + var screenshot = SentryScreenshot.CreateNewScreenshotTexture2D(_options); + var screenshotBytes = screenshot.EncodeToJPG(_options.ScreenshotCompression); + UnityEngine.Object.Destroy(screenshot); + + hint = SentryHint.WithAttachments( new SentryAttachment( AttachmentType.Default, - new ByteAttachmentContent(SentryScreenshot.Capture(_options)), + new ByteAttachmentContent(screenshotBytes), "screenshot.jpg", - "image/jpeg")) - : null; + "image/jpeg")); + } Sentry.SentrySdk.CurrentHub.CaptureFeedback(message, email, name, hint: hint); } diff --git a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs index 83e7e5e72..09253e5b2 100644 --- a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.IO; using NUnit.Framework; using Sentry.Unity.Tests.Stubs; using UnityEngine; @@ -11,26 +12,26 @@ public class ScreenshotEventProcessorTests { private class TestScreenshotEventProcessor : ScreenshotEventProcessor { - public Func CaptureScreenshotFunc { get; set; } + public Func CreateScreenshotFunc { get; set; } public Action CaptureAttachmentAction { get; set; } public Func WaitForEndOfFrameFunc { get; set; } public TestScreenshotEventProcessor(SentryUnityOptions options, ISentryMonoBehaviour sentryMonoBehaviour) : base(options, sentryMonoBehaviour) { - CaptureScreenshotFunc = _ => [0xFF, 0xD8, 0xFF]; + CreateScreenshotFunc = _ => new Texture2D(1, 1); CaptureAttachmentAction = (_, _) => { }; WaitForEndOfFrameFunc = () => new YieldInstruction(); } - internal override byte[] CaptureScreenshot(SentryUnityOptions options) - => CaptureScreenshotFunc.Invoke(options); + internal override Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions options) + => CreateScreenshotFunc.Invoke(options); internal override void CaptureAttachment(SentryId eventId, SentryAttachment attachment) => CaptureAttachmentAction(eventId, attachment); internal override YieldInstruction WaitForEndOfFrame() - => WaitForEndOfFrameFunc!.Invoke(); + => WaitForEndOfFrameFunc.Invoke(); } [Test] public void Process_FirstCallInAFrame_StartsCoroutine() @@ -81,10 +82,10 @@ public IEnumerator Process_CalledMultipleTimesQuickly_OnlyExecutesScreenshotCapt var screenshotProcessor = new TestScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); var screenshotCaptureCallCount = 0; - screenshotProcessor.CaptureScreenshotFunc = _ => + screenshotProcessor.CreateScreenshotFunc = _ => { screenshotCaptureCallCount++; - return [0]; + return new Texture2D(1, 1); }; var attachmentCaptureCallCount = 0; @@ -107,12 +108,12 @@ public IEnumerator Process_CalledMultipleTimesQuickly_OnlyExecutesScreenshotCapt } [UnityTest] - public IEnumerator Process_EmptyScreenshotData_SkipsAttachmentCapture() + public IEnumerator Process_ScreenshotCaptureThrowsException_HandlesGracefully() { var sentryMonoBehaviour = GetTestMonoBehaviour(); var screenshotProcessor = new TestScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); - screenshotProcessor.CaptureScreenshotFunc = _ => []; + screenshotProcessor.CreateScreenshotFunc = _ => throw new Exception("Screenshot capture failed"); var attachmentCaptureCallCount = 0; screenshotProcessor.CaptureAttachmentAction = (_, _) => @@ -120,19 +121,233 @@ public IEnumerator Process_EmptyScreenshotData_SkipsAttachmentCapture() attachmentCaptureCallCount++; }; + var sentryEvent = new SentryEvent(); + screenshotProcessor.Process(sentryEvent); + + // Wait for the coroutine to complete - need to wait for processing + yield return null; + yield return null; + + Assert.IsTrue(sentryMonoBehaviour.StartCoroutineCalled); + Assert.AreEqual(0, attachmentCaptureCallCount); + } + + [UnityTest] + public IEnumerator Process_BeforeSendScreenshotCallback_ReceivesScreenshotAndEvent() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + Texture2D? receivedScreenshot = null; + SentryEvent? receivedEvent = null; + + options.SetBeforeSendScreenshot((screenshot, @event) => + { + receivedScreenshot = screenshot; + receivedEvent = @event; + return screenshot; + }); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + var eventId = SentryId.Create(); var sentryEvent = new SentryEvent(eventId: eventId); screenshotProcessor.Process(sentryEvent); - // Wait for the coroutine to complete - need to wait for processing yield return null; yield return null; - Assert.IsTrue(sentryMonoBehaviour.StartCoroutineCalled); + Assert.NotNull(receivedScreenshot); + Assert.NotNull(receivedEvent); + Assert.AreEqual(eventId, receivedEvent!.EventId); + } + + [UnityTest] + public IEnumerator Process_BeforeSendScreenshotCallback_ReturnsNull_SkipsAttachment() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + options.SetBeforeSendScreenshot((_, _) => null); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + + var attachmentCaptureCallCount = 0; + screenshotProcessor.CaptureAttachmentAction = (_, _) => + { + attachmentCaptureCallCount++; + }; + + var sentryEvent = new SentryEvent(); + screenshotProcessor.Process(sentryEvent); + + yield return null; + yield return null; + Assert.AreEqual(0, attachmentCaptureCallCount); } + [UnityTest] + public IEnumerator Process_BeforeSendScreenshotCallbackReturnsNewTexture_AttachesNewTexture() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + var newTexture = new Texture2D(10, 10); + var newTextureBytes = newTexture.EncodeToJPG(options.ScreenshotCompression); + var beforeSendInvoked = false; + + options.SetBeforeSendScreenshot((_, _) => + { + beforeSendInvoked = true; + return newTexture; + }); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + + var attachmentCaptured = false; + byte[]? capturedBytes = null; + + screenshotProcessor.CaptureAttachmentAction = (_, attachment) => + { + attachmentCaptured = true; + if (attachment.Content is ByteAttachmentContent byteContent) + { + using var stream = byteContent.GetStream(); + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + capturedBytes = memoryStream.ToArray(); + } + }; + + screenshotProcessor.Process(new SentryEvent()); + + yield return null; + yield return null; + + Assert.IsTrue(beforeSendInvoked); // Sanity Check + Assert.IsTrue(attachmentCaptured); // Sanity Check + Assert.NotNull(capturedBytes); + Assert.AreEqual(newTextureBytes.Length, capturedBytes!.Length); + + UnityEngine.Object.Destroy(newTexture); + } + + [UnityTest] + public IEnumerator Process_BeforeSendScreenshotCallbackModifiesTexture_UsesModifiedTexture() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + var callbackInvoked = false; + byte[]? modifiedTextureBytes = null; + + options.SetBeforeSendScreenshot((screenshot, @event) => + { + callbackInvoked = true; + + // User modifies the texture in place + + var pixels = screenshot.GetPixels(); + for (var i = 0; i < pixels.Length; i++) + { + pixels[i] = Color.red; + } + screenshot.SetPixels(pixels); + screenshot.Apply(); + + modifiedTextureBytes = screenshot.EncodeToJPG(options.ScreenshotCompression); + + return screenshot; + }); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + + var attachmentCaptured = false; + byte[]? capturedBytes = null; + + screenshotProcessor.CaptureAttachmentAction = (_, attachment) => + { + attachmentCaptured = true; + if (attachment.Content is ByteAttachmentContent byteContent) + { + using var stream = byteContent.GetStream(); + using var memoryStream = new MemoryStream(); + stream.CopyTo(memoryStream); + capturedBytes = memoryStream.ToArray(); + } + }; + + screenshotProcessor.Process(new SentryEvent()); + + yield return null; + yield return null; + + Assert.IsTrue(callbackInvoked); // Sanity Check + Assert.IsTrue(attachmentCaptured); // Sanity Check + Assert.NotNull(modifiedTextureBytes); + Assert.NotNull(capturedBytes); + Assert.AreEqual(modifiedTextureBytes!.Length, capturedBytes!.Length); + } + + [UnityTest] + public IEnumerator Process_BeforeCaptureScreenshotCallback_ReturnsFalse_SkipsCapture() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + options.SetBeforeCaptureScreenshot(() => false); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + + var screenshotCaptureCallCount = 0; + screenshotProcessor.CreateScreenshotFunc = _ => + { + screenshotCaptureCallCount++; + return new Texture2D(1, 1); + }; + + screenshotProcessor.Process(new SentryEvent()); + + yield return null; + yield return null; + + // BeforeCaptureScreenshot should prevent capture entirely + Assert.AreEqual(0, screenshotCaptureCallCount); + } + + [UnityTest] + public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesScreenshot() + { + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var options = new SentryUnityOptions(); + + var callbackInvoked = false; + options.SetBeforeCaptureScreenshot(() => + { + callbackInvoked = true; + return true; + }); + + var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); + + var screenshotCaptureCallCount = 0; + screenshotProcessor.CreateScreenshotFunc = _ => + { + screenshotCaptureCallCount++; + return new Texture2D(1, 1); + }; + + screenshotProcessor.Process(new SentryEvent()); + + yield return null; + yield return null; + + Assert.IsTrue(callbackInvoked); + Assert.AreEqual(1, screenshotCaptureCallCount); + } + private static TestSentryMonoBehaviour GetTestMonoBehaviour() { var gameObject = new GameObject("ScreenshotProcessorTest"); diff --git a/test/Sentry.Unity.Tests/SentryScreenshotTests.cs b/test/Sentry.Unity.Tests/SentryScreenshotTests.cs index e7ca887e1..5bde51c5c 100644 --- a/test/Sentry.Unity.Tests/SentryScreenshotTests.cs +++ b/test/Sentry.Unity.Tests/SentryScreenshotTests.cs @@ -24,11 +24,10 @@ public void CaptureScreenshot_QualitySet_ScreenshotDoesNotExceedDimensionLimit(S { var options = new SentryUnityOptions { ScreenshotQuality = quality }; - var bytes = SentryScreenshot.Capture(options, 2000, 2000); - var texture = new Texture2D(1, 1); // Size does not matter. Will be overwritten by loading - texture.LoadImage(bytes); + var texture = SentryScreenshot.CreateNewScreenshotTexture2D(options, 2000, 2000); Assert.IsTrue(texture.width <= maximumAllowedDimension && texture.height <= maximumAllowedDimension); + Object.Destroy(texture); } [Test] @@ -37,10 +36,9 @@ public void CaptureScreenshot_QualitySetToFull_ScreenshotInFullSize() var testScreenSize = 2000; var options = new SentryUnityOptions { ScreenshotQuality = ScreenshotQuality.Full }; - var bytes = SentryScreenshot.Capture(options, testScreenSize, testScreenSize); - var texture = new Texture2D(1, 1); // Size does not matter. Will be overwritten by loading - texture.LoadImage(bytes); + var texture = SentryScreenshot.CreateNewScreenshotTexture2D(options, testScreenSize, testScreenSize); Assert.IsTrue(texture.width == testScreenSize && texture.height == testScreenSize); + Object.Destroy(texture); } } From e16853a01b2cf08ff80152ef3a218d31f30bf213 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 17:27:30 +0100 Subject: [PATCH 2/9] Added beforeSend for ViewHierarchy --- src/Sentry.Unity/SentryUnityOptions.cs | 22 +++- src/Sentry.Unity/UnityViewHierarchyNode.cs | 2 +- .../ViewHierarchyEventProcessor.cs | 34 +++-- .../ViewHierarchyEventProcessorTests.cs | 117 +++++++++++++++++- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 8304e6529..83756dfac 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -281,9 +281,23 @@ public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) BeforeCaptureScreenshotInternal = beforeAttachScreenshot; } - private Func? _beforeCaptureViewHierarchy; + internal Func? BeforeCaptureViewHierarchyInternal { get; private set; } - internal Func? BeforeCaptureViewHierarchyInternal => _beforeCaptureViewHierarchy; + internal Func? BeforeSendViewHierarchyInternal { get; private set; } + + /// + /// Configures a callback to modify or discard view hierarchy before it is sent. + /// + /// + /// This callback receives the captured view hierarchy before JSON serialization. + /// You can modify the hierarchy structure (remove nodes, filter sensitive info, etc.) + /// and return it, or return null to discard. + /// + /// The callback function to invoke before sending view hierarchy. + public void SetBeforeSendViewHierarchy(Func beforeSendViewHierarchy) + { + BeforeSendViewHierarchyInternal = beforeSendViewHierarchy; + } /// /// Configures a callback function to be invoked before capturing and attaching the view hierarchy to an event. @@ -292,9 +306,9 @@ public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) /// This callback will get invoked right before the view hierarchy gets taken. If the view hierarchy should not /// be taken return `false`. /// - public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) + public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) { - _beforeCaptureViewHierarchy = beforeAttachViewHierarchy; + BeforeCaptureViewHierarchyInternal = beforeAttachViewHierarchy; } // Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor). diff --git a/src/Sentry.Unity/UnityViewHierarchyNode.cs b/src/Sentry.Unity/UnityViewHierarchyNode.cs index 08da6e468..1dfc09cb6 100644 --- a/src/Sentry.Unity/UnityViewHierarchyNode.cs +++ b/src/Sentry.Unity/UnityViewHierarchyNode.cs @@ -4,7 +4,7 @@ namespace Sentry.Unity; -internal class UnityViewHierarchyNode : ViewHierarchyNode +public class UnityViewHierarchyNode : ViewHierarchyNode { public string? Tag { get; set; } public string? Position { get; set; } diff --git a/src/Sentry.Unity/ViewHierarchyEventProcessor.cs b/src/Sentry.Unity/ViewHierarchyEventProcessor.cs index 0ef342a0b..7cb170808 100644 --- a/src/Sentry.Unity/ViewHierarchyEventProcessor.cs +++ b/src/Sentry.Unity/ViewHierarchyEventProcessor.cs @@ -30,27 +30,43 @@ public ViewHierarchyEventProcessor(SentryUnityOptions sentryOptions) return @event; } - if (_options.BeforeCaptureViewHierarchyInternal?.Invoke() is not false) + // Old callback: decide before capture + if (_options.BeforeCaptureViewHierarchyInternal?.Invoke(@event) is false) { - hint.AddAttachment(CaptureViewHierarchy(), "view-hierarchy.json", AttachmentType.ViewHierarchy, "application/json"); + _options.DiagnosticLogger?.LogInfo("Hierarchy capture skipped by BeforeCaptureViewHierarchy callback."); + return @event; } - else + + // Create view hierarchy + var viewHierarchy = CreateViewHierarchy( + _options.MaxViewHierarchyRootObjects, + _options.MaxViewHierarchyObjectChildCount, + _options.MaxViewHierarchyDepth); + + // Apply new callback if configured + if (_options.BeforeSendViewHierarchyInternal != null) { - _options.DiagnosticLogger?.LogInfo("Hierarchy capture skipped by BeforeAttachViewHierarchy callback."); + viewHierarchy = _options.BeforeSendViewHierarchyInternal(viewHierarchy, @event); + + if (viewHierarchy == null) + { + _options.DiagnosticLogger?.LogInfo("View hierarchy discarded by BeforeSendViewHierarchy callback."); + return @event; + } } + // Serialize and attach + var bytes = SerializeViewHierarchy(viewHierarchy); + hint.AddAttachment(bytes, "view-hierarchy.json", AttachmentType.ViewHierarchy, "application/json"); + return @event; } - internal byte[] CaptureViewHierarchy() + internal byte[] SerializeViewHierarchy(ViewHierarchy viewHierarchy) { using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream); - var viewHierarchy = CreateViewHierarchy( - _options.MaxViewHierarchyRootObjects, - _options.MaxViewHierarchyObjectChildCount, - _options.MaxViewHierarchyDepth); viewHierarchy.WriteTo(writer, _options.DiagnosticLogger); writer.Flush(); diff --git a/test/Sentry.Unity.Tests/ViewHierarchyEventProcessorTests.cs b/test/Sentry.Unity.Tests/ViewHierarchyEventProcessorTests.cs index 93ec9dfc4..57e9ea3f6 100644 --- a/test/Sentry.Unity.Tests/ViewHierarchyEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ViewHierarchyEventProcessorTests.cs @@ -65,7 +65,7 @@ public void Process_IsNonMainThread_DoesNotAddViewHierarchyToHint() [TestCase(false)] public void Process_BeforeCaptureViewHierarchyCallbackProvided_RespectViewHierarchyCaptureDecision(bool captureViewHierarchy) { - _fixture.Options.SetBeforeCaptureViewHierarchy(() => captureViewHierarchy); + _fixture.Options.SetBeforeCaptureViewHierarchy(_ => captureViewHierarchy); var sut = _fixture.GetSut(); var sentryEvent = new SentryEvent(); var hint = new SentryHint(); @@ -76,11 +76,12 @@ public void Process_BeforeCaptureViewHierarchyCallbackProvided_RespectViewHierar } [Test] - public void CaptureViewHierarchy_ReturnsNonNullOrEmptyByteArray() + public void SerializeViewHierarchy_ReturnsNonNullOrEmptyByteArray() { var sut = _fixture.GetSut(); + var viewHierarchy = sut.CreateViewHierarchy(1, 1, 1); - var byteArray = sut.CaptureViewHierarchy(); + var byteArray = sut.SerializeViewHierarchy(viewHierarchy); Assert.That(byteArray, Is.Not.Null); Assert.That(byteArray.Length, Is.GreaterThan(0)); @@ -184,6 +185,116 @@ public void CreateNode_LessChildrenThanMaxChildCount_CapturesViewHierarchy() Assert.AreEqual(3, root.Children[0].Children.Count); } + [Test] + public void Process_BeforeSendViewHierarchyCallback_ReceivesViewHierarchyAndEvent() + { + ViewHierarchy? receivedViewHierarchy = null; + SentryEvent? receivedEvent = null; + + _fixture.Options.SetBeforeSendViewHierarchy((viewHierarchy, @event) => + { + receivedViewHierarchy = viewHierarchy; + receivedEvent = @event; + return viewHierarchy; + }); + + var sut = _fixture.GetSut(); + var sentryEvent = new SentryEvent(); + var hint = new SentryHint(); + + sut.Process(sentryEvent, hint); + + Assert.NotNull(receivedViewHierarchy); + Assert.NotNull(receivedEvent); + Assert.AreEqual(sentryEvent.EventId, receivedEvent!.EventId); + Assert.AreEqual(1, hint.Attachments.Count); + } + + [Test] + public void Process_BeforeSendViewHierarchyCallback_ReturnsNull_SkipsAttachment() + { + _fixture.Options.SetBeforeSendViewHierarchy((_, _) => null); + + var sut = _fixture.GetSut(); + var sentryEvent = new SentryEvent(); + var hint = new SentryHint(); + + sut.Process(sentryEvent, hint); + + Assert.AreEqual(0, hint.Attachments.Count); + } + + [Test] + public void Process_BeforeSendViewHierarchyCallback_ModifiesHierarchy_UsesModifiedVersion() + { + var callbackInvoked = false; + + _fixture.Options.SetBeforeSendViewHierarchy((viewHierarchy, @event) => + { + callbackInvoked = true; + // Remove all children from the root window + viewHierarchy.Windows[0].Children.Clear(); + return viewHierarchy; + }); + + var sut = _fixture.GetSut(); + + // Create some game objects so there's something to remove + for (var i = 0; i < 3; i++) + { + var _ = new GameObject($"GameObject_{i}"); + } + + var sentryEvent = new SentryEvent(); + var hint = new SentryHint(); + + sut.Process(sentryEvent, hint); + + Assert.IsTrue(callbackInvoked); + Assert.AreEqual(1, hint.Attachments.Count); + + // Verify the modification was applied by deserializing + var attachment = hint.Attachments.First(); + var content = attachment.Content as ByteAttachmentContent; + Assert.NotNull(content); + + using var stream = content!.GetStream(); + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + // The JSON should show an empty children array + Assert.That(json, Does.Contain("\"children\":[]")); + } + + [Test] + public void Process_BeforeSendViewHierarchyCallback_ReturnsDifferentHierarchy_UsesNewHierarchy() + { + var newHierarchy = new ViewHierarchy("CustomRenderingSystem"); + newHierarchy.Windows.Add(new UnityViewHierarchyNode("CustomWindow")); + + _fixture.Options.SetBeforeSendViewHierarchy((_, _) => newHierarchy); + + var sut = _fixture.GetSut(); + var sentryEvent = new SentryEvent(); + var hint = new SentryHint(); + + sut.Process(sentryEvent, hint); + + Assert.AreEqual(1, hint.Attachments.Count); + + // Verify the new hierarchy was used + var attachment = hint.Attachments.First(); + var content = attachment.Content as ByteAttachmentContent; + Assert.NotNull(content); + + using var stream = content!.GetStream(); + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.That(json, Does.Contain("CustomRenderingSystem")); + Assert.That(json, Does.Contain("CustomWindow")); + } + private void CreateTestHierarchy(int remainingDepth, int childCount, Transform parent) { remainingDepth--; From bfd81b9e72a02f532c44453342e1b9cf8aaa534e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 17:29:38 +0100 Subject: [PATCH 3/9] Added event to the beforeCapture callback --- src/Sentry.Unity/ScreenshotEventProcessor.cs | 2 +- src/Sentry.Unity/SentryUnityOptions.cs | 26 ++++++++++++++----- .../ScreenshotEventProcessorTests.cs | 4 +-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Sentry.Unity/ScreenshotEventProcessor.cs b/src/Sentry.Unity/ScreenshotEventProcessor.cs index 154cab4fd..d9416205c 100644 --- a/src/Sentry.Unity/ScreenshotEventProcessor.cs +++ b/src/Sentry.Unity/ScreenshotEventProcessor.cs @@ -44,7 +44,7 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryEvent @event) Texture2D? screenshot = null; try { - if (_options.BeforeCaptureScreenshotInternal?.Invoke() is false) + if (_options.BeforeCaptureScreenshotInternal?.Invoke(@event) is false) { yield break; } diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 8304e6529..368a88ade 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -252,7 +252,7 @@ public sealed class SentryUnityOptions : SentryOptions /// public new StackTraceMode StackTraceMode { get; private set; } - internal Func? BeforeCaptureScreenshotInternal { get; private set; } + internal Func? BeforeCaptureScreenshotInternal { get; private set; } internal Func? BeforeSendScreenshotInternal { get; private set; } @@ -276,14 +276,28 @@ public void SetBeforeSendScreenshot(Func bef /// This callback will get invoked right before a screenshot gets taken. If the screenshot should not /// be taken return `false`. /// - public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) + public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) { BeforeCaptureScreenshotInternal = beforeAttachScreenshot; } - private Func? _beforeCaptureViewHierarchy; + internal Func? BeforeCaptureViewHierarchyInternal { get; private set; } - internal Func? BeforeCaptureViewHierarchyInternal => _beforeCaptureViewHierarchy; + internal Func? BeforeSendViewHierarchyInternal { get; private set; } + + /// + /// Configures a callback to modify or discard view hierarchy before it is sent. + /// + /// + /// This callback receives the captured view hierarchy before JSON serialization. + /// You can modify the hierarchy structure (remove nodes, filter sensitive info, etc.) + /// and return it, or return null to discard. + /// + /// The callback function to invoke before sending view hierarchy. + public void SetBeforeSendViewHierarchy(Func beforeSendViewHierarchy) + { + BeforeSendViewHierarchyInternal = beforeSendViewHierarchy; + } /// /// Configures a callback function to be invoked before capturing and attaching the view hierarchy to an event. @@ -292,9 +306,9 @@ public void SetBeforeCaptureScreenshot(Func beforeAttachScreenshot) /// This callback will get invoked right before the view hierarchy gets taken. If the view hierarchy should not /// be taken return `false`. /// - public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) + public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) { - _beforeCaptureViewHierarchy = beforeAttachViewHierarchy; + BeforeCaptureViewHierarchyInternal = beforeAttachViewHierarchy; } // Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor). diff --git a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs index 09253e5b2..81891bc47 100644 --- a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs @@ -297,7 +297,7 @@ public IEnumerator Process_BeforeCaptureScreenshotCallback_ReturnsFalse_SkipsCap var sentryMonoBehaviour = GetTestMonoBehaviour(); var options = new SentryUnityOptions(); - options.SetBeforeCaptureScreenshot(() => false); + options.SetBeforeCaptureScreenshot(_ => false); var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour); @@ -324,7 +324,7 @@ public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesSc var options = new SentryUnityOptions(); var callbackInvoked = false; - options.SetBeforeCaptureScreenshot(() => + options.SetBeforeCaptureScreenshot(_ => { callbackInvoked = true; return true; From b319902e63b688483af0111ebabf5786927d42b4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 17:59:28 +0100 Subject: [PATCH 4/9] Updated CHANGELOG.md --- CHANGELOG.md | 15 ++++++++++ src/Sentry.Unity/SentryUnityOptions.cs | 40 +++++++++----------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7073780df..c82236063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## Unreleased +### Breaking Changes + +- `SetBeforeCaptureScreenshot` signature changed from `Func` to `Func`, now receiving the event that + triggered the screenshot capture. This allows context-aware decisions before capture begins. ([#2428](https://github.com/getsentry/sentry-unity/pull/2428)) + +### Features + +- Added `SetBeforeSendScreenshot(Func)` callback that provides the captured screenshot as a + `Texture2D` before JPEG compression. ([#2428](https://github.com/getsentry/sentry-unity/pull/2428)) + This enables: + - **Modifying** the screenshot in-place (e.g., blurring sensitive UI areas, redacting PII) + - **Replacing** the screenshot with a different `Texture2D` + - **Discarding** the screenshot by returning `null` + - Access to the event context for conditional processing + ### Dependencies - Bump Cocoa SDK from v8.57.2 to v8.57.3 ([#2424](https://github.com/getsentry/sentry-unity/pull/2424)) diff --git a/src/Sentry.Unity/SentryUnityOptions.cs b/src/Sentry.Unity/SentryUnityOptions.cs index 368a88ade..a152d5458 100644 --- a/src/Sentry.Unity/SentryUnityOptions.cs +++ b/src/Sentry.Unity/SentryUnityOptions.cs @@ -254,21 +254,6 @@ public sealed class SentryUnityOptions : SentryOptions internal Func? BeforeCaptureScreenshotInternal { get; private set; } - internal Func? BeforeSendScreenshotInternal { get; private set; } - - /// - /// Configures a callback to modify or discard screenshots before they are sent. - /// - /// - /// This callback receives the captured screenshot as a Texture2D before JPEG compression. - /// You can modify the texture (blur areas, redact PII, etc.) and return it, or return null to discard. - /// - /// The callback function to invoke before sending screenshots. - public void SetBeforeSendScreenshot(Func beforeSendScreenshot) - { - BeforeSendScreenshotInternal = beforeSendScreenshot; - } - /// /// Configures a callback function to be invoked before capturing and attaching a screenshot to an event. /// @@ -281,24 +266,25 @@ public void SetBeforeCaptureScreenshot(Func beforeAttachScree BeforeCaptureScreenshotInternal = beforeAttachScreenshot; } - internal Func? BeforeCaptureViewHierarchyInternal { get; private set; } - - internal Func? BeforeSendViewHierarchyInternal { get; private set; } + internal Func? BeforeSendScreenshotInternal { get; private set; } /// - /// Configures a callback to modify or discard view hierarchy before it is sent. + /// Configures a callback to modify or discard screenshots before they are sent. /// /// - /// This callback receives the captured view hierarchy before JSON serialization. - /// You can modify the hierarchy structure (remove nodes, filter sensitive info, etc.) - /// and return it, or return null to discard. + /// This callback receives the captured screenshot as a Texture2D before JPEG compression. + /// You can modify the texture (blur areas, redact PII, etc.) and return it, or return null to discard. /// - /// The callback function to invoke before sending view hierarchy. - public void SetBeforeSendViewHierarchy(Func beforeSendViewHierarchy) + /// The callback function to invoke before sending screenshots. + public void SetBeforeSendScreenshot(Func beforeSendScreenshot) { - BeforeSendViewHierarchyInternal = beforeSendViewHierarchy; + BeforeSendScreenshotInternal = beforeSendScreenshot; } + private Func? _beforeCaptureViewHierarchy; + + internal Func? BeforeCaptureViewHierarchyInternal => _beforeCaptureViewHierarchy; + /// /// Configures a callback function to be invoked before capturing and attaching the view hierarchy to an event. /// @@ -306,9 +292,9 @@ public void SetBeforeSendViewHierarchy(Func - public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) + public void SetBeforeCaptureViewHierarchy(Func beforeAttachViewHierarchy) { - BeforeCaptureViewHierarchyInternal = beforeAttachViewHierarchy; + _beforeCaptureViewHierarchy = beforeAttachViewHierarchy; } // Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor). From be091126a5010c6d313ce8ef26ca78286b39c055 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 18:04:30 +0100 Subject: [PATCH 5/9] Logging --- src/Sentry.Unity/SentryScreenshot.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.Unity/SentryScreenshot.cs b/src/Sentry.Unity/SentryScreenshot.cs index 3e9c163aa..2e829d8ce 100644 --- a/src/Sentry.Unity/SentryScreenshot.cs +++ b/src/Sentry.Unity/SentryScreenshot.cs @@ -66,7 +66,7 @@ internal static Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions option screenshot.ReadPixels(new Rect(0, 0, width, height), 0, 0); screenshot.Apply(); - options.LogDebug("Screenshot captured at {0}x{1}: {2} bytes", width, height); + options.LogDebug("Screenshot captured at {0}x{1}.", width, height); return screenshot; } From db7057c86c4b573fe50b094921d76e871b423528 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 18:08:16 +0100 Subject: [PATCH 6/9] Updated CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c82236063..8abe78ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - `SetBeforeCaptureScreenshot` signature changed from `Func` to `Func`, now receiving the event that triggered the screenshot capture. This allows context-aware decisions before capture begins. ([#2428](https://github.com/getsentry/sentry-unity/pull/2428)) +- `SetBeforeCaptureViewHierarchy` signature changed from `Func` to `Func`, now receiving the event that + triggered the view hierarchy capture. This allows context-aware decisions before capture begins. ([#2429](https://github.com/getsentry/sentry-unity/pull/2429)) ### Features @@ -16,6 +18,8 @@ - **Replacing** the screenshot with a different `Texture2D` - **Discarding** the screenshot by returning `null` - Access to the event context for conditional processing +- Added `SetBeforeSendViewHierarchy(Func)` callback that provides the captured + `ViewHierarchy` to be modified before compression. ([#2429](https://github.com/getsentry/sentry-unity/pull/2429)) ### Dependencies From 954b03a95ceb7eb08209f52044230333b1e4a61d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 18:11:01 +0100 Subject: [PATCH 7/9] Updated CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8abe78ec7..d3bd2eab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ - **Replacing** the screenshot with a different `Texture2D` - **Discarding** the screenshot by returning `null` - Access to the event context for conditional processing -- Added `SetBeforeSendViewHierarchy(Func)` callback that provides the captured +- Added `SetBeforeSendViewHierarchy(Func)` callback that provides the captured `ViewHierarchy` to be modified before compression. ([#2429](https://github.com/getsentry/sentry-unity/pull/2429)) ### Dependencies From 32fa0f4d3e49d4e075ca49e959042e26fbe8270c Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Nov 2025 18:11:27 +0100 Subject: [PATCH 8/9] . --- src/Sentry.Unity/ViewHierarchyEventProcessor.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Sentry.Unity/ViewHierarchyEventProcessor.cs b/src/Sentry.Unity/ViewHierarchyEventProcessor.cs index 7cb170808..9cf4ab9f0 100644 --- a/src/Sentry.Unity/ViewHierarchyEventProcessor.cs +++ b/src/Sentry.Unity/ViewHierarchyEventProcessor.cs @@ -30,20 +30,17 @@ public ViewHierarchyEventProcessor(SentryUnityOptions sentryOptions) return @event; } - // Old callback: decide before capture if (_options.BeforeCaptureViewHierarchyInternal?.Invoke(@event) is false) { _options.DiagnosticLogger?.LogInfo("Hierarchy capture skipped by BeforeCaptureViewHierarchy callback."); return @event; } - // Create view hierarchy var viewHierarchy = CreateViewHierarchy( _options.MaxViewHierarchyRootObjects, _options.MaxViewHierarchyObjectChildCount, _options.MaxViewHierarchyDepth); - // Apply new callback if configured if (_options.BeforeSendViewHierarchyInternal != null) { viewHierarchy = _options.BeforeSendViewHierarchyInternal(viewHierarchy, @event); @@ -55,7 +52,6 @@ public ViewHierarchyEventProcessor(SentryUnityOptions sentryOptions) } } - // Serialize and attach var bytes = SerializeViewHierarchy(viewHierarchy); hint.AddAttachment(bytes, "view-hierarchy.json", AttachmentType.ViewHierarchy, "application/json"); From 56b543c8992005f0e3365996c0eb91bf3e73f94e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 21 Nov 2025 13:03:12 +0100 Subject: [PATCH 9/9] merged --- src/Sentry.Unity/SentryUnitySdk.cs | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/Sentry.Unity/SentryUnitySdk.cs b/src/Sentry.Unity/SentryUnitySdk.cs index 8fa24fc99..5d741c9ad 100644 --- a/src/Sentry.Unity/SentryUnitySdk.cs +++ b/src/Sentry.Unity/SentryUnitySdk.cs @@ -124,16 +124,30 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad SentryHint? hint = null; if (addScreenshot) { - var screenshot = SentryScreenshot.CreateNewScreenshotTexture2D(_options); - var screenshotBytes = screenshot.EncodeToJPG(_options.ScreenshotCompression); - UnityEngine.Object.Destroy(screenshot); - - hint = SentryHint.WithAttachments( - new SentryAttachment( - AttachmentType.Default, - new ByteAttachmentContent(screenshotBytes), - "screenshot.jpg", - "image/jpeg")); + Texture2D? screenshot = null; + + try + { + screenshot = SentryScreenshot.CreateNewScreenshotTexture2D(_options); + var screenshotBytes = screenshot.EncodeToJPG(_options.ScreenshotCompression); + + if (screenshotBytes.Length > 0) + { + hint = SentryHint.WithAttachments( + new SentryAttachment( + AttachmentType.Default, + new ByteAttachmentContent(screenshotBytes), + "screenshot.jpg", + "image/jpeg")); + } + } + finally + { + if (screenshot) + { + UnityEngine.Object.Destroy(screenshot); + } + } } Sentry.SentrySdk.CurrentHub.CaptureFeedback(message, email, name, hint: hint);