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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@

## Unreleased

### Fixes

- The SDK no longer sends screenshot attachments for events that were dropped during processing (e.g., by `BeforeSend` or sampling) ([#2661](https://github.com/getsentry/sentry-unity/pull/2661))

### Dependencies

- Bump .NET SDK from v6.3.1 to v6.5.0 ([#2661](https://github.com/getsentry/sentry-unity/pull/2661))
- [changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md#650)
- [diff](https://github.com/getsentry/sentry-dotnet/compare/6.3.1...6.5.0)
- Bump Native SDK from v0.13.5 to v0.14.0 ([#2660](https://github.com/getsentry/sentry-unity/pull/2660))
- [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0140)
- [diff](https://github.com/getsentry/sentry-native/compare/0.13.5...0.14.0)
Expand Down
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"sdk": {
"version": "10.0.201",
"workloadVersion": "10.0.201",
"version": "10.0.203",
"workloadVersion": "10.0.203",
"rollForward": "disable",
"allowPrerelease": false
}
Expand Down
6 changes: 6 additions & 0 deletions src/Sentry.Unity/ScreenshotEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ internal IEnumerator CaptureScreenshotCoroutine(SentryEvent @event)
Texture2D? screenshot = null;
try
{
if (!@event.IsCaptured)
{
_options.LogDebug("Skipping screenshot for event {0}. Event was not captured.", @event.EventId);
yield break;
}

if (_options.BeforeCaptureScreenshotInternal?.Invoke(@event) is false)
{
yield break;
Expand Down
2 changes: 1 addition & 1 deletion src/sentry-dotnet
Submodule sentry-dotnet updated 154 files
165 changes: 154 additions & 11 deletions test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +11,26 @@ namespace Sentry.Unity.Tests;

public class ScreenshotEventProcessorTests
{
/// <summary>
/// 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.
/// </summary>
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<SentryUnityOptions, Texture2D> CreateScreenshotFunc { get; set; }
Expand Down Expand Up @@ -59,7 +80,7 @@ public IEnumerator Process_ExecutesCoroutine_CapturesScreenshotAndCapturesAttach
};

var eventId = SentryId.Create();
var sentryEvent = new SentryEvent(eventId: eventId);
var sentryEvent = new SentryEvent(eventId: eventId) { IsCaptured = true };

screenshotProcessor.Process(sentryEvent);

Expand Down Expand Up @@ -95,9 +116,9 @@ public IEnumerator Process_CalledMultipleTimesQuickly_OnlyExecutesScreenshotCapt
};

// Process multiple events quickly (before any coroutine can complete)
screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });

// Wait for the coroutine to complete - need to wait for processing
yield return null;
Expand All @@ -121,7 +142,7 @@ public IEnumerator Process_ScreenshotCaptureThrowsException_HandlesGracefully()
attachmentCaptureCallCount++;
};

var sentryEvent = new SentryEvent();
var sentryEvent = new SentryEvent { IsCaptured = true };
screenshotProcessor.Process(sentryEvent);

// Wait for the coroutine to complete - need to wait for processing
Expand Down Expand Up @@ -151,7 +172,7 @@ public IEnumerator Process_BeforeSendScreenshotCallback_ReceivesScreenshotAndEve
var screenshotProcessor = new TestScreenshotEventProcessor(options, sentryMonoBehaviour);

var eventId = SentryId.Create();
var sentryEvent = new SentryEvent(eventId: eventId);
var sentryEvent = new SentryEvent(eventId: eventId) { IsCaptured = true };

screenshotProcessor.Process(sentryEvent);

Expand Down Expand Up @@ -179,7 +200,7 @@ public IEnumerator Process_BeforeSendScreenshotCallback_ReturnsNull_SkipsAttachm
attachmentCaptureCallCount++;
};

var sentryEvent = new SentryEvent();
var sentryEvent = new SentryEvent { IsCaptured = true };
screenshotProcessor.Process(sentryEvent);

yield return null;
Expand Down Expand Up @@ -221,7 +242,7 @@ public IEnumerator Process_BeforeSendScreenshotCallbackReturnsNewTexture_Attache
}
};

screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });

yield return null;
yield return null;
Expand Down Expand Up @@ -279,7 +300,7 @@ public IEnumerator Process_BeforeSendScreenshotCallbackModifiesTexture_UsesModif
}
};

screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });

yield return null;
yield return null;
Expand Down Expand Up @@ -308,7 +329,7 @@ public IEnumerator Process_BeforeCaptureScreenshotCallback_ReturnsFalse_SkipsCap
return new Texture2D(1, 1);
};

screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });

yield return null;
yield return null;
Expand Down Expand Up @@ -339,7 +360,7 @@ public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesSc
return new Texture2D(1, 1);
};

screenshotProcessor.Process(new SentryEvent());
screenshotProcessor.Process(new SentryEvent { IsCaptured = true });

yield return null;
yield return null;
Expand All @@ -348,6 +369,128 @@ public IEnumerator Process_BeforeCaptureScreenshotCallbackReturnsTrue_CapturesSc
Assert.AreEqual(1, screenshotCaptureCallCount);
}

[UnityTest]
public IEnumerator Process_EventNotCaptured_SkipsAttachment()
{
var sentryMonoBehaviour = GetTestMonoBehaviour();
var screenshotProcessor = new TestScreenshotEventProcessor(new SentryUnityOptions(), sentryMonoBehaviour);

var screenshotCaptureCallCount = 0;
screenshotProcessor.CreateScreenshotFunc = _ =>
{
screenshotCaptureCallCount++;
return new Texture2D(1, 1);
};

var attachmentCaptureCallCount = 0;
screenshotProcessor.CaptureAttachmentAction = (_, _) =>
{
attachmentCaptureCallCount++;
};

screenshotProcessor.Process(new SentryEvent());

yield return null;
yield return null;

Assert.AreEqual(0, screenshotCaptureCallCount);
Assert.AreEqual(0, attachmentCaptureCallCount);
}

[UnityTest]
public IEnumerator Process_EventCapturedSuccessfully_ScreenshotAttachmentIsSent()
{
// 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 IsCaptured flag
// is doing its job.

var httpHandler = new TestHttpClientHandler("ScreenshotSuccessTest");
var sentryMonoBehaviour = GetTestMonoBehaviour();

var options = new SentryUnityOptions(application: new TestApplication())
{
Dsn = SentryTests.TestDsn,
CreateHttpMessageHandler = () => httpHandler
};

// 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
{
// Event goes through the full DoSendEvent pipeline and is captured successfully.
// DoSendEvent sets @event.IsCaptured = true after CaptureEnvelope succeeds.
var capturedId = SentrySdk.CaptureMessage("test message");
Assert.AreNotEqual(SentryId.Empty, capturedId, "Sanity check: event should be captured");

// Wait for the screenshot coroutine to complete
yield return null;
yield return null;

// 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 IsCaptured 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, IsCaptured stays false
var capturedId = SentrySdk.CaptureMessage("test message");
Assert.AreEqual(SentryId.Empty, capturedId, "Sanity check: before_send should drop events");

// Wait for the screenshot coroutine to complete
yield return null;
yield return null;

// 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");
}
finally
{
SentrySdk.Close();
}
}

private static TestSentryMonoBehaviour GetTestMonoBehaviour()
{
var gameObject = new GameObject("ScreenshotProcessorTest");
Expand Down
Loading