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

## Unreleased

### Breaking Changes

- `SetBeforeCaptureScreenshot` signature changed from `Func<bool>` to `Func<SentryEvent, bool>`, 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<Texture2D, SentryEvent, Texture2D?>)` 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))
Expand Down
47 changes: 37 additions & 10 deletions src/Sentry.Unity/ScreenshotEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,53 @@ 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.");

// WaitForEndOfFrame does not work in headless mode so we're making it configurable for CI.
// See https://docs.unity3d.com/6000.1/Documentation/ScriptReference/WaitForEndOfFrame.html
yield return WaitForEndOfFrame();

Texture2D? screenshot = null;
try
{
if (_options.BeforeCaptureScreenshotInternal?.Invoke() is false)
if (_options.BeforeCaptureScreenshotInternal?.Invoke(@event) is false)
{
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;
}

Expand All @@ -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)
{
Expand All @@ -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);
Expand Down
22 changes: 7 additions & 15 deletions src/Sentry.Unity/SentryScreenshot.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Sentry.Extensibility;
using UnityEngine;

namespace Sentry.Unity;
Expand All @@ -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
Expand All @@ -36,15 +37,14 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height
}
}

Texture2D? screenshot = null;
RenderTexture? renderTextureFull = null;
RenderTexture? renderTextureResized = null;
var previousRenderTexture = RenderTexture.active;

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);
Expand All @@ -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}.", 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
{
Expand All @@ -86,11 +83,6 @@ internal static byte[] Capture(SentryUnityOptions options, int width, int height
{
RenderTexture.ReleaseTemporary(renderTextureResized);
}

if (screenshot)
{
Object.Destroy(screenshot);
}
}
}
}
23 changes: 18 additions & 5 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,7 @@ public sealed class SentryUnityOptions : SentryOptions
/// </summary>
public new StackTraceMode StackTraceMode { get; private set; }

private Func<bool>? _beforeCaptureScreenshot;

internal Func<bool>? BeforeCaptureScreenshotInternal => _beforeCaptureScreenshot;
internal Func<SentryEvent, bool>? BeforeCaptureScreenshotInternal { get; private set; }

/// <summary>
/// Configures a callback function to be invoked before capturing and attaching a screenshot to an event.
Expand All @@ -263,9 +261,24 @@ public sealed class SentryUnityOptions : SentryOptions
/// This callback will get invoked right before a screenshot gets taken. If the screenshot should not
/// be taken return `false`.
/// </remarks>
public void SetBeforeCaptureScreenshot(Func<bool> beforeAttachScreenshot)
public void SetBeforeCaptureScreenshot(Func<SentryEvent, bool> beforeAttachScreenshot)
{
BeforeCaptureScreenshotInternal = beforeAttachScreenshot;
}

internal Func<Texture2D, SentryEvent, Texture2D?>? BeforeSendScreenshotInternal { get; private set; }

/// <summary>
/// Configures a callback to modify or discard screenshots before they are sent.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
/// <param name="beforeSendScreenshot">The callback function to invoke before sending screenshots.</param>
public void SetBeforeSendScreenshot(Func<Texture2D, SentryEvent, Texture2D?> beforeSendScreenshot)
{
_beforeCaptureScreenshot = beforeAttachScreenshot;
BeforeSendScreenshotInternal = beforeSendScreenshot;
}

private Func<bool>? _beforeCaptureViewHierarchy;
Expand Down
36 changes: 28 additions & 8 deletions src/Sentry.Unity/SentryUnitySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,34 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad
return;
}

var hint = addScreenshot
? SentryHint.WithAttachments(
new SentryAttachment(
AttachmentType.Default,
new ByteAttachmentContent(SentryScreenshot.Capture(_options)),
"screenshot.jpg",
"image/jpeg"))
: null;
SentryHint? hint = null;
if (addScreenshot)
{
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);
}
Expand Down
Loading