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");