Skip to content

Commit cc445ae

Browse files
authored
feat: Added BeforeSend for ViewHierarchy Capture (#2429)
1 parent 95dc419 commit cc445ae

File tree

5 files changed

+158
-17
lines changed

5 files changed

+158
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
- `SetBeforeCaptureScreenshot` signature changed from `Func<bool>` to `Func<SentryEvent, bool>`, now receiving the event that
88
triggered the screenshot capture. This allows context-aware decisions before capture begins. ([#2428](https://github.com/getsentry/sentry-unity/pull/2428))
9+
- `SetBeforeCaptureViewHierarchy` signature changed from `Func<bool>` to `Func<SentryEvent, bool>`, now receiving the event that
10+
triggered the view hierarchy capture. This allows context-aware decisions before capture begins. ([#2429](https://github.com/getsentry/sentry-unity/pull/2429))
911

1012
### Features
1113

@@ -16,6 +18,8 @@
1618
- **Replacing** the screenshot with a different `Texture2D`
1719
- **Discarding** the screenshot by returning `null`
1820
- Access to the event context for conditional processing
21+
- Added `SetBeforeSendViewHierarchy(Func<ViewHierarchy, SentryEvent, ViewHierarchy?>)` callback that provides the captured
22+
`ViewHierarchy` to be modified before compression. ([#2429](https://github.com/getsentry/sentry-unity/pull/2429))
1923

2024
### Dependencies
2125

src/Sentry.Unity/SentryUnityOptions.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,23 @@ public void SetBeforeSendScreenshot(Func<Texture2D, SentryEvent, Texture2D?> bef
281281
BeforeSendScreenshotInternal = beforeSendScreenshot;
282282
}
283283

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

286-
internal Func<bool>? BeforeCaptureViewHierarchyInternal => _beforeCaptureViewHierarchy;
286+
internal Func<ViewHierarchy, SentryEvent, ViewHierarchy?>? BeforeSendViewHierarchyInternal { get; private set; }
287+
288+
/// <summary>
289+
/// Configures a callback to modify or discard view hierarchy before it is sent.
290+
/// </summary>
291+
/// <remarks>
292+
/// This callback receives the captured view hierarchy before JSON serialization.
293+
/// You can modify the hierarchy structure (remove nodes, filter sensitive info, etc.)
294+
/// and return it, or return null to discard.
295+
/// </remarks>
296+
/// <param name="beforeSendViewHierarchy">The callback function to invoke before sending view hierarchy.</param>
297+
public void SetBeforeSendViewHierarchy(Func<ViewHierarchy, SentryEvent, ViewHierarchy?> beforeSendViewHierarchy)
298+
{
299+
BeforeSendViewHierarchyInternal = beforeSendViewHierarchy;
300+
}
287301

288302
/// <summary>
289303
/// Configures a callback function to be invoked before capturing and attaching the view hierarchy to an event.
@@ -292,9 +306,9 @@ public void SetBeforeSendScreenshot(Func<Texture2D, SentryEvent, Texture2D?> bef
292306
/// This callback will get invoked right before the view hierarchy gets taken. If the view hierarchy should not
293307
/// be taken return `false`.
294308
/// </remarks>
295-
public void SetBeforeCaptureViewHierarchy(Func<bool> beforeAttachViewHierarchy)
309+
public void SetBeforeCaptureViewHierarchy(Func<SentryEvent, bool> beforeAttachViewHierarchy)
296310
{
297-
_beforeCaptureViewHierarchy = beforeAttachViewHierarchy;
311+
BeforeCaptureViewHierarchyInternal = beforeAttachViewHierarchy;
298312
}
299313

300314
// Initialized by native SDK binding code to set the User.ID in .NET (UnityEventProcessor).

src/Sentry.Unity/UnityViewHierarchyNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Sentry.Unity;
66

7-
internal class UnityViewHierarchyNode : ViewHierarchyNode
7+
public class UnityViewHierarchyNode : ViewHierarchyNode
88
{
99
public string? Tag { get; set; }
1010
public string? Position { get; set; }

src/Sentry.Unity/ViewHierarchyEventProcessor.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,39 @@ public ViewHierarchyEventProcessor(SentryUnityOptions sentryOptions)
3030
return @event;
3131
}
3232

33-
if (_options.BeforeCaptureViewHierarchyInternal?.Invoke() is not false)
33+
if (_options.BeforeCaptureViewHierarchyInternal?.Invoke(@event) is false)
3434
{
35-
hint.AddAttachment(CaptureViewHierarchy(), "view-hierarchy.json", AttachmentType.ViewHierarchy, "application/json");
35+
_options.DiagnosticLogger?.LogInfo("Hierarchy capture skipped by BeforeCaptureViewHierarchy callback.");
36+
return @event;
3637
}
37-
else
38+
39+
var viewHierarchy = CreateViewHierarchy(
40+
_options.MaxViewHierarchyRootObjects,
41+
_options.MaxViewHierarchyObjectChildCount,
42+
_options.MaxViewHierarchyDepth);
43+
44+
if (_options.BeforeSendViewHierarchyInternal != null)
3845
{
39-
_options.DiagnosticLogger?.LogInfo("Hierarchy capture skipped by BeforeAttachViewHierarchy callback.");
46+
viewHierarchy = _options.BeforeSendViewHierarchyInternal(viewHierarchy, @event);
47+
48+
if (viewHierarchy == null)
49+
{
50+
_options.DiagnosticLogger?.LogInfo("View hierarchy discarded by BeforeSendViewHierarchy callback.");
51+
return @event;
52+
}
4053
}
4154

55+
var bytes = SerializeViewHierarchy(viewHierarchy);
56+
hint.AddAttachment(bytes, "view-hierarchy.json", AttachmentType.ViewHierarchy, "application/json");
57+
4258
return @event;
4359
}
4460

45-
internal byte[] CaptureViewHierarchy()
61+
internal byte[] SerializeViewHierarchy(ViewHierarchy viewHierarchy)
4662
{
4763
using var stream = new MemoryStream();
4864
using var writer = new Utf8JsonWriter(stream);
4965

50-
var viewHierarchy = CreateViewHierarchy(
51-
_options.MaxViewHierarchyRootObjects,
52-
_options.MaxViewHierarchyObjectChildCount,
53-
_options.MaxViewHierarchyDepth);
5466
viewHierarchy.WriteTo(writer, _options.DiagnosticLogger);
5567

5668
writer.Flush();

test/Sentry.Unity.Tests/ViewHierarchyEventProcessorTests.cs

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public void Process_IsNonMainThread_DoesNotAddViewHierarchyToHint()
6565
[TestCase(false)]
6666
public void Process_BeforeCaptureViewHierarchyCallbackProvided_RespectViewHierarchyCaptureDecision(bool captureViewHierarchy)
6767
{
68-
_fixture.Options.SetBeforeCaptureViewHierarchy(() => captureViewHierarchy);
68+
_fixture.Options.SetBeforeCaptureViewHierarchy(_ => captureViewHierarchy);
6969
var sut = _fixture.GetSut();
7070
var sentryEvent = new SentryEvent();
7171
var hint = new SentryHint();
@@ -76,11 +76,12 @@ public void Process_BeforeCaptureViewHierarchyCallbackProvided_RespectViewHierar
7676
}
7777

7878
[Test]
79-
public void CaptureViewHierarchy_ReturnsNonNullOrEmptyByteArray()
79+
public void SerializeViewHierarchy_ReturnsNonNullOrEmptyByteArray()
8080
{
8181
var sut = _fixture.GetSut();
82+
var viewHierarchy = sut.CreateViewHierarchy(1, 1, 1);
8283

83-
var byteArray = sut.CaptureViewHierarchy();
84+
var byteArray = sut.SerializeViewHierarchy(viewHierarchy);
8485

8586
Assert.That(byteArray, Is.Not.Null);
8687
Assert.That(byteArray.Length, Is.GreaterThan(0));
@@ -184,6 +185,116 @@ public void CreateNode_LessChildrenThanMaxChildCount_CapturesViewHierarchy()
184185
Assert.AreEqual(3, root.Children[0].Children.Count);
185186
}
186187

188+
[Test]
189+
public void Process_BeforeSendViewHierarchyCallback_ReceivesViewHierarchyAndEvent()
190+
{
191+
ViewHierarchy? receivedViewHierarchy = null;
192+
SentryEvent? receivedEvent = null;
193+
194+
_fixture.Options.SetBeforeSendViewHierarchy((viewHierarchy, @event) =>
195+
{
196+
receivedViewHierarchy = viewHierarchy;
197+
receivedEvent = @event;
198+
return viewHierarchy;
199+
});
200+
201+
var sut = _fixture.GetSut();
202+
var sentryEvent = new SentryEvent();
203+
var hint = new SentryHint();
204+
205+
sut.Process(sentryEvent, hint);
206+
207+
Assert.NotNull(receivedViewHierarchy);
208+
Assert.NotNull(receivedEvent);
209+
Assert.AreEqual(sentryEvent.EventId, receivedEvent!.EventId);
210+
Assert.AreEqual(1, hint.Attachments.Count);
211+
}
212+
213+
[Test]
214+
public void Process_BeforeSendViewHierarchyCallback_ReturnsNull_SkipsAttachment()
215+
{
216+
_fixture.Options.SetBeforeSendViewHierarchy((_, _) => null);
217+
218+
var sut = _fixture.GetSut();
219+
var sentryEvent = new SentryEvent();
220+
var hint = new SentryHint();
221+
222+
sut.Process(sentryEvent, hint);
223+
224+
Assert.AreEqual(0, hint.Attachments.Count);
225+
}
226+
227+
[Test]
228+
public void Process_BeforeSendViewHierarchyCallback_ModifiesHierarchy_UsesModifiedVersion()
229+
{
230+
var callbackInvoked = false;
231+
232+
_fixture.Options.SetBeforeSendViewHierarchy((viewHierarchy, @event) =>
233+
{
234+
callbackInvoked = true;
235+
// Remove all children from the root window
236+
viewHierarchy.Windows[0].Children.Clear();
237+
return viewHierarchy;
238+
});
239+
240+
var sut = _fixture.GetSut();
241+
242+
// Create some game objects so there's something to remove
243+
for (var i = 0; i < 3; i++)
244+
{
245+
var _ = new GameObject($"GameObject_{i}");
246+
}
247+
248+
var sentryEvent = new SentryEvent();
249+
var hint = new SentryHint();
250+
251+
sut.Process(sentryEvent, hint);
252+
253+
Assert.IsTrue(callbackInvoked);
254+
Assert.AreEqual(1, hint.Attachments.Count);
255+
256+
// Verify the modification was applied by deserializing
257+
var attachment = hint.Attachments.First();
258+
var content = attachment.Content as ByteAttachmentContent;
259+
Assert.NotNull(content);
260+
261+
using var stream = content!.GetStream();
262+
using var reader = new StreamReader(stream);
263+
var json = reader.ReadToEnd();
264+
265+
// The JSON should show an empty children array
266+
Assert.That(json, Does.Contain("\"children\":[]"));
267+
}
268+
269+
[Test]
270+
public void Process_BeforeSendViewHierarchyCallback_ReturnsDifferentHierarchy_UsesNewHierarchy()
271+
{
272+
var newHierarchy = new ViewHierarchy("CustomRenderingSystem");
273+
newHierarchy.Windows.Add(new UnityViewHierarchyNode("CustomWindow"));
274+
275+
_fixture.Options.SetBeforeSendViewHierarchy((_, _) => newHierarchy);
276+
277+
var sut = _fixture.GetSut();
278+
var sentryEvent = new SentryEvent();
279+
var hint = new SentryHint();
280+
281+
sut.Process(sentryEvent, hint);
282+
283+
Assert.AreEqual(1, hint.Attachments.Count);
284+
285+
// Verify the new hierarchy was used
286+
var attachment = hint.Attachments.First();
287+
var content = attachment.Content as ByteAttachmentContent;
288+
Assert.NotNull(content);
289+
290+
using var stream = content!.GetStream();
291+
using var reader = new StreamReader(stream);
292+
var json = reader.ReadToEnd();
293+
294+
Assert.That(json, Does.Contain("CustomRenderingSystem"));
295+
Assert.That(json, Does.Contain("CustomWindow"));
296+
}
297+
187298
private void CreateTestHierarchy(int remainingDepth, int childCount, Transform parent)
188299
{
189300
remainingDepth--;

0 commit comments

Comments
 (0)