Skip to content

fix: Screenshot Capture #2240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7c90568
initial idea
bitsandfoxes Jul 10, 2025
98a7988
Format code
getsentry-bot Jul 10, 2025
a3a70a4
Merge branch 'main' into fix/screenshot-capture
bitsandfoxes Jul 15, 2025
f204618
Merge branch 'fix/screenshot-capture' of https://github.com/getsentry…
bitsandfoxes Jul 15, 2025
8f74147
capture and send screenshot
bitsandfoxes Jul 16, 2025
8f852e0
merged main
bitsandfoxes Jul 16, 2025
35a172d
bumped implementation in .NET
bitsandfoxes Jul 18, 2025
11548fe
updated to make it work with tests
bitsandfoxes Jul 18, 2025
91aa77f
Format code
getsentry-bot Jul 18, 2025
113e40e
moved screenshot capture logic back into the processor
bitsandfoxes Jul 18, 2025
271ef13
cleanup
bitsandfoxes Jul 18, 2025
c4c5a96
Updated CHANGELOG.md
bitsandfoxes Jul 18, 2025
7e183a1
added delay in tests to fix flake
bitsandfoxes Jul 21, 2025
41d69fc
updated the smoketester to handle the delayed screenshot capture
bitsandfoxes Jul 21, 2025
521298d
updated success/fail checks
bitsandfoxes Jul 21, 2025
8dfd665
more CI work
bitsandfoxes Jul 21, 2025
e2e83a8
bumped .NET SDK
bitsandfoxes Jul 21, 2025
072d2d3
.
bitsandfoxes Jul 21, 2025
176de44
cleaned up the smoketester
bitsandfoxes Jul 21, 2025
778e03d
.
bitsandfoxes Jul 21, 2025
34b03bd
increased smoke test run timeout
bitsandfoxes Jul 21, 2025
f4ed1fa
waitforseconds for smoketester instead
bitsandfoxes Jul 22, 2025
38d2738
debug logs
bitsandfoxes Jul 22, 2025
9015cd5
no coroutine for smoketesting no more
bitsandfoxes Jul 22, 2025
4e634d1
increase http request interception timeout
bitsandfoxes Jul 22, 2025
a9dfcb1
it needs to be a coroutine
bitsandfoxes Jul 22, 2025
e05a579
if waitfor does not work what about yield null?
bitsandfoxes Jul 22, 2025
26be76b
force opengl on linux
bitsandfoxes Jul 22, 2025
1cd0c2c
keep focus
bitsandfoxes Jul 22, 2025
b8fdb41
smoke tester cleanup
bitsandfoxes Jul 22, 2025
2e24049
is it the focus thing?
bitsandfoxes Jul 22, 2025
8043f3c
one more time. does it really require the sentrymonobehaviour?
bitsandfoxes Jul 22, 2025
6c9b8c8
going with buildtime settings here
bitsandfoxes Jul 22, 2025
c0f38f0
final pass
bitsandfoxes Jul 22, 2025
73d3589
chore: Added Unity `6.1` to CI (#2130)
bitsandfoxes Jul 22, 2025
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
55 changes: 8 additions & 47 deletions src/Sentry.Unity/ScreenshotEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,22 @@

namespace Sentry.Unity;

public class ScreenshotEventProcessor : ISentryEventProcessorWithHint
public class ScreenshotEventProcessor : ISentryEventProcessor
{
private readonly SentryUnityOptions _options;
private readonly IApplication _application;
public ScreenshotEventProcessor(SentryUnityOptions sentryOptions) : this(sentryOptions, null) { }
private readonly SentryMonoBehaviour _sentryMonoBehaviour;

internal ScreenshotEventProcessor(SentryUnityOptions sentryOptions, IApplication? application)
{
_options = sentryOptions;
_application = application ?? ApplicationAdapter.Instance;
}
public ScreenshotEventProcessor(SentryUnityOptions sentryOptions) : this(sentryOptions, SentryMonoBehaviour.Instance) { }

public SentryEvent? Process(SentryEvent @event)
internal ScreenshotEventProcessor(SentryUnityOptions sentryOptions, SentryMonoBehaviour? sentryMonoBehaviour)
{
return @event;
_options = sentryOptions;
_sentryMonoBehaviour = sentryMonoBehaviour ?? SentryMonoBehaviour.Instance;
}

public SentryEvent? Process(SentryEvent @event, SentryHint hint)
public SentryEvent Process(SentryEvent @event)
{
// save event id
// wait for end of frame
// check if last id is event it
// send screenshot

// add workitem: screentshot for ID xxx
// sdk integration checking for work: if ID got sent, follow up with screenshot

if (!MainThreadData.IsMainThread())
{
_options.DiagnosticLogger?.LogDebug("Screenshot capture skipped. Can't capture screenshots on other than the main thread.");
return @event;
}

if (_options.BeforeCaptureScreenshotInternal?.Invoke() is not false)
{
if (_application.IsEditor)
{
_options.DiagnosticLogger?.LogInfo("Screenshot capture skipped. Capturing screenshots it not supported in the Editor");
return @event;
}

if (Screen.width == 0 || Screen.height == 0)
{
_options.DiagnosticLogger?.LogWarning("Can't capture screenshots on a screen with a resolution of '{0}x{1}'.", Screen.width, Screen.height);
}
else
{
hint.AddAttachment(SentryScreenshot.Capture(_options), "screenshot.jpg", contentType: "image/jpeg");
}
}
else
{
_options.DiagnosticLogger?.LogInfo("Screenshot capture skipped by BeforeAttachScreenshot callback.");
}

_sentryMonoBehaviour.CaptureScreenshotForEvent(_options, @event.EventId);
return @event;
}
}
24 changes: 24 additions & 0 deletions src/Sentry.Unity/SentryMonoBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections;
using Sentry.Unity.Integrations;
using UnityEngine;

Expand Down Expand Up @@ -112,3 +113,26 @@ private void Awake()
DontDestroyOnLoad(gameObject);
}
}

/// <summary>
/// A MonoBehaviour that captures screenshots
/// </summary>
public partial class SentryMonoBehaviour
{
// Todo: keep track of event ID to not capture double/more than once per frame

public void CaptureScreenshotForEvent(SentryUnityOptions options, SentryId eventId)
{
StartCoroutine(CaptureScreenshot(options, eventId));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method returns before the coroutine starts/ends, right? how do we know the screenshot will be taken before the event is created?

Copy link
Contributor Author

@bitsandfoxes bitsandfoxes Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ScreenshotProcessor forwards the call to the SentryMonoBehaviour that starts the coroutine for the current EndOfFrame.

}

private IEnumerator CaptureScreenshot(SentryUnityOptions options, SentryId eventId)
{
yield return new WaitForEndOfFrame();

SentryScreenshot.Capture(options);

// Todo: figure out how to capture an event with a screenshot attachment from out here
var sentryEvent = new SentryEvent(eventId: eventId);
}
}
65 changes: 2 additions & 63 deletions test/Sentry.Unity.Tests/ScreenshotEventProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ private class Fixture
public SentryUnityOptions Options = new() { AttachScreenshot = true };
public TestApplication TestApplication = new();

public ScreenshotEventProcessor GetSut() => new(Options, TestApplication);
public ScreenshotEventProcessor GetSut() => new(Options);
}

private Fixture _fixture = null!;
Expand All @@ -28,66 +28,5 @@ public void TearDown()
}
}

[Test]
public void Process_IsMainThread_AddsScreenshotToHint()
{
_fixture.TestApplication.IsEditor = false;
var sut = _fixture.GetSut();
var sentryEvent = new SentryEvent();
var hint = new SentryHint();

sut.Process(sentryEvent, hint);

Assert.AreEqual(1, hint.Attachments.Count);
}

[Test]
public void Process_IsNonMainThread_DoesNotAddScreenshotToHint()
{
var sut = _fixture.GetSut();
var sentryEvent = new SentryEvent();
var hint = new SentryHint();

new Thread(() =>
{
Thread.CurrentThread.IsBackground = true;
var stream = sut.Process(sentryEvent, hint);

Assert.AreEqual(0, hint.Attachments.Count);
}).Start();
}

[Test]
[TestCase(true)]
[TestCase(false)]
public void Process_BeforeCaptureScreenshotCallbackProvided_RespectsScreenshotCaptureDecision(bool captureScreenshot)
{
_fixture.TestApplication.IsEditor = false;
_fixture.Options.SetBeforeCaptureScreenshot(() => captureScreenshot);
var sut = _fixture.GetSut();
var sentryEvent = new SentryEvent();
var hint = new SentryHint();

sut.Process(sentryEvent, hint);

Assert.AreEqual(captureScreenshot ? 1 : 0, hint.Attachments.Count);
}

[Test]
[TestCase(true, 0)]
[TestCase(false, 1)]
public void Process_InEditorEnvironment_DoesNotCaptureScreenshot(bool isEditor, int expectedAttachmentCount)
{
// Arrange
_fixture.TestApplication.IsEditor = isEditor;
var sut = _fixture.GetSut();
var sentryEvent = new SentryEvent();
var hint = new SentryHint();

// Act
sut.Process(sentryEvent, hint);

// Assert
Assert.AreEqual(expectedAttachmentCount, hint.Attachments.Count);
}
// Todo: Add tests that verify passing the capture on to the MonoBehaviour
}