From ad6b397991e0d930d6030f10170b6b3c0df31e55 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:55:47 +0200 Subject: [PATCH 1/2] Add test validating incorrect behaviour --- .../ScreenshotEventProcessorTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs index 81891bc47..988de97c1 100644 --- a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs @@ -10,6 +10,26 @@ namespace Sentry.Unity.Tests; public class ScreenshotEventProcessorTests { + /// + /// Subclass that mocks screenshot capture and WaitForEndOfFrame but uses the REAL + /// CaptureAttachment implementation (via Hub.CaptureAttachment), allowing us to verify + /// that the attachment envelope actually reaches the HTTP transport. + /// + private class RealCaptureScreenshotEventProcessor : ScreenshotEventProcessor + { + public RealCaptureScreenshotEventProcessor(SentryUnityOptions options, ISentryMonoBehaviour sentryMonoBehaviour) + : base(options, sentryMonoBehaviour) { } + + internal override Texture2D CreateNewScreenshotTexture2D(SentryUnityOptions options) + => new Texture2D(1, 1); + + internal override YieldInstruction WaitForEndOfFrame() + => new YieldInstruction(); + + // CaptureAttachment is intentionally NOT overridden — the base implementation + // calls Hub.CaptureAttachment which sends a standalone attachment envelope. + } + private class TestScreenshotEventProcessor : ScreenshotEventProcessor { public Func CreateScreenshotFunc { get; set; } @@ -348,6 +368,51 @@ public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesSc Assert.AreEqual(1, screenshotCaptureCallCount); } + [UnityTest] + public IEnumerator Process_EventDroppedByBeforeSend_ScreenshotAttachmentIsNotSent() + { + // When before_send drops an event (returns null), the screenshot should NOT be sent. + // The screenshot processor's async coroutine must not send orphaned attachment envelopes + // that count against quota but have no associated event in Sentry. + + var httpHandler = new TestHttpClientHandler("ScreenshotBeforeSendTest"); + var sdkOptions = new SentryUnityOptions(application: new TestApplication()) + { + Dsn = SentryTests.TestDsn, + CreateHttpMessageHandler = () => httpHandler + }; + sdkOptions.SetBeforeSend((_, _) => null); // Drop all events + SentrySdk.Init(sdkOptions); + + try + { + // Sanity check: verify before_send drops events + var capturedEventId = SentrySdk.CaptureMessage("test message"); + Assert.AreEqual(SentryId.Empty, capturedEventId, "Sanity check: before_send should drop events"); + + // Create a processor that mocks screenshot capture but uses the real CaptureAttachment + // pathway (Hub.CaptureAttachment → Envelope.FromAttachment → HTTP transport) + var sentryMonoBehaviour = GetTestMonoBehaviour(); + var processor = new RealCaptureScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); + + var sentryEvent = new SentryEvent(); + processor.Process(sentryEvent); + + // Wait for the coroutine to complete (fires at end of frame) + yield return null; + yield return null; + + // The screenshot attachment must not be sent when the event was dropped by before_send + var screenshotRequest = httpHandler.GetEvent("screenshot.jpg", TimeSpan.FromSeconds(2)); + Assert.IsEmpty(screenshotRequest, + "Screenshot attachment should not be sent when before_send drops the event"); + } + finally + { + SentrySdk.Close(); + } + } + private static TestSentryMonoBehaviour GetTestMonoBehaviour() { var gameObject = new GameObject("ScreenshotProcessorTest"); From c772bfbec0998bd272453f8183da59a129d8f032 Mon Sep 17 00:00:00 2001 From: JoshuaMoelans <60878493+JoshuaMoelans@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:21:51 +0200 Subject: [PATCH 2/2] Rewrite screenshot test to exercise full DoSendEvent pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous test called processor.Process() directly, bypassing DoSendEvent entirely. It passed after the WasCaptured fix only because the flag defaults to false — not because DoSendEvent actively left it false after before_send dropped the event. The new tests register the screenshot processor via AddEventProcessor and trigger events through SentrySdk.CaptureMessage, so the event flows through the real pipeline. A positive control test confirms screenshots are sent when events are captured. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ScreenshotEventProcessorTests.cs | 88 +++++++++++++++---- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs index 988de97c1..de781814f 100644 --- a/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs +++ b/test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.IO; +using System.Text.RegularExpressions; using NUnit.Framework; using Sentry.Unity.Tests.Stubs; using UnityEngine; @@ -369,40 +370,89 @@ public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesSc } [UnityTest] - public IEnumerator Process_EventDroppedByBeforeSend_ScreenshotAttachmentIsNotSent() + public IEnumerator Process_EventCapturedSuccessfully_ScreenshotAttachmentIsSent() { - // When before_send drops an event (returns null), the screenshot should NOT be sent. - // The screenshot processor's async coroutine must not send orphaned attachment envelopes - // that count against quota but have no associated event in Sentry. + // Positive control: when the event IS captured, the screenshot coroutine should send + // the attachment. This validates the test infrastructure so the negative test below + // is meaningful — if this test passes but the next one doesn't, the WasCaptured flag + // is doing its job. - var httpHandler = new TestHttpClientHandler("ScreenshotBeforeSendTest"); - var sdkOptions = new SentryUnityOptions(application: new TestApplication()) + var httpHandler = new TestHttpClientHandler("ScreenshotSuccessTest"); + var sentryMonoBehaviour = GetTestMonoBehaviour(); + + var options = new SentryUnityOptions(application: new TestApplication()) { Dsn = SentryTests.TestDsn, CreateHttpMessageHandler = () => httpHandler }; - sdkOptions.SetBeforeSend((_, _) => null); // Drop all events - SentrySdk.Init(sdkOptions); + + // Register test screenshot processor as an event processor — it will be called + // during DoSendEvent → ProcessEvent, just like the real ScreenshotEventProcessor. + options.AddEventProcessor(new RealCaptureScreenshotEventProcessor(options, sentryMonoBehaviour)); + + SentrySdk.Init(options); try { - // Sanity check: verify before_send drops events - var capturedEventId = SentrySdk.CaptureMessage("test message"); - Assert.AreEqual(SentryId.Empty, capturedEventId, "Sanity check: before_send should drop events"); + // Event goes through the full DoSendEvent pipeline and is captured successfully. + // DoSendEvent sets @event.WasCaptured = true after CaptureEnvelope succeeds. + var capturedId = SentrySdk.CaptureMessage("test message"); + Assert.AreNotEqual(SentryId.Empty, capturedId, "Sanity check: event should be captured"); - // Create a processor that mocks screenshot capture but uses the real CaptureAttachment - // pathway (Hub.CaptureAttachment → Envelope.FromAttachment → HTTP transport) - var sentryMonoBehaviour = GetTestMonoBehaviour(); - var processor = new RealCaptureScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour); + // Wait for the screenshot coroutine to complete + yield return null; + yield return null; - var sentryEvent = new SentryEvent(); - processor.Process(sentryEvent); + // Screenshot envelope should reach the transport + var screenshotRequest = httpHandler.GetEvent("screenshot.jpg", TimeSpan.FromSeconds(2)); + Assert.IsNotEmpty(screenshotRequest, + "Screenshot attachment should be sent when the event is captured successfully"); + } + finally + { + SentrySdk.Close(); + } + } + + [UnityTest] + public IEnumerator Process_EventDroppedByBeforeSend_ScreenshotAttachmentIsNotSent() + { + // Full pipeline test: the event goes through DoSendEvent where before_send drops it. + // The screenshot coroutine (queued during ProcessEvent, before the drop decision) + // must check WasCaptured and skip — no orphaned attachment envelope. + + var httpHandler = new TestHttpClientHandler("ScreenshotBeforeSendTest"); + var sentryMonoBehaviour = GetTestMonoBehaviour(); + + var options = new SentryUnityOptions(application: new TestApplication()) + { + Dsn = SentryTests.TestDsn, + CreateHttpMessageHandler = () => httpHandler + }; + + // Register test screenshot processor — called during DoSendEvent → ProcessEvent + options.AddEventProcessor(new RealCaptureScreenshotEventProcessor(options, sentryMonoBehaviour)); + + // Drop all events via before_send + options.SetBeforeSend((_, _) => null); + + SentrySdk.Init(options); + + try + { + // CaptureMessage goes through the full DoSendEvent pipeline: + // ProcessEvent → screenshot processor queues coroutine with @event in closure + // DoBeforeSend → returns null → event dropped, WasCaptured stays false + var capturedId = SentrySdk.CaptureMessage("test message"); + Assert.AreEqual(SentryId.Empty, capturedId, "Sanity check: before_send should drop events"); - // Wait for the coroutine to complete (fires at end of frame) + // Wait for the screenshot coroutine to complete yield return null; yield return null; - // The screenshot attachment must not be sent when the event was dropped by before_send + // No screenshot envelope should reach the transport. + // GetEvent logs Debug.LogError on timeout — tell the test runner this is expected. + LogAssert.Expect(LogType.Error, new Regex("timed out")); var screenshotRequest = httpHandler.GetEvent("screenshot.jpg", TimeSpan.FromSeconds(2)); Assert.IsEmpty(screenshotRequest, "Screenshot attachment should not be sent when before_send drops the event");