Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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}: {2} bytes", 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);
}
}
}
}
43 changes: 35 additions & 8 deletions src/Sentry.Unity/SentryUnityOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,22 @@ public sealed class SentryUnityOptions : SentryOptions
/// </summary>
public new StackTraceMode StackTraceMode { get; private set; }

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

internal Func<bool>? BeforeCaptureScreenshotInternal => _beforeCaptureScreenshot;
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)
{
BeforeSendScreenshotInternal = beforeSendScreenshot;
}

/// <summary>
/// Configures a callback function to be invoked before capturing and attaching a screenshot to an event.
Expand All @@ -263,14 +276,28 @@ 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)
{
_beforeCaptureScreenshot = beforeAttachScreenshot;
BeforeCaptureScreenshotInternal = beforeAttachScreenshot;
}

private Func<bool>? _beforeCaptureViewHierarchy;
internal Func<SentryEvent, bool>? BeforeCaptureViewHierarchyInternal { get; private set; }

internal Func<bool>? BeforeCaptureViewHierarchyInternal => _beforeCaptureViewHierarchy;
internal Func<ViewHierarchy, SentryEvent, ViewHierarchy?>? BeforeSendViewHierarchyInternal { get; private set; }

/// <summary>
/// Configures a callback to modify or discard view hierarchy before it is sent.
/// </summary>
/// <remarks>
/// This callback receives the captured view hierarchy before JSON serialization.
/// You can modify the hierarchy structure (remove nodes, filter sensitive info, etc.)
/// and return it, or return null to discard.
/// </remarks>
/// <param name="beforeSendViewHierarchy">The callback function to invoke before sending view hierarchy.</param>
public void SetBeforeSendViewHierarchy(Func<ViewHierarchy, SentryEvent, ViewHierarchy?> beforeSendViewHierarchy)
{
BeforeSendViewHierarchyInternal = beforeSendViewHierarchy;
}

/// <summary>
/// Configures a callback function to be invoked before capturing and attaching the view hierarchy to an event.
Expand All @@ -279,9 +306,9 @@ public void SetBeforeCaptureScreenshot(Func<bool> beforeAttachScreenshot)
/// This callback will get invoked right before the view hierarchy gets taken. If the view hierarchy should not
/// be taken return `false`.
/// </remarks>
public void SetBeforeCaptureViewHierarchy(Func<bool> beforeAttachViewHierarchy)
public void SetBeforeCaptureViewHierarchy(Func<SentryEvent, bool> beforeAttachViewHierarchy)
{
_beforeCaptureViewHierarchy = beforeAttachViewHierarchy;
BeforeCaptureViewHierarchyInternal = beforeAttachViewHierarchy;
}

// Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor).
Expand Down
16 changes: 11 additions & 5 deletions src/Sentry.Unity/SentryUnitySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,20 @@ public void CaptureFeedback(string message, string? email, string? name, bool ad
return;
}

var hint = addScreenshot
? SentryHint.WithAttachments(
SentryHint? hint = null;
if (addScreenshot)
{
var screenshot = SentryScreenshot.CreateNewScreenshotTexture2D(_options);
var screenshotBytes = screenshot.EncodeToJPG(_options.ScreenshotCompression);
UnityEngine.Object.Destroy(screenshot);

hint = SentryHint.WithAttachments(
new SentryAttachment(
AttachmentType.Default,
new ByteAttachmentContent(SentryScreenshot.Capture(_options)),
new ByteAttachmentContent(screenshotBytes),
"screenshot.jpg",
"image/jpeg"))
: null;
"image/jpeg"));
}

Sentry.SentrySdk.CurrentHub.CaptureFeedback(message, email, name, hint: hint);
}
Expand Down
Loading
Loading