Skip to content

Commit 6a67f5e

Browse files
Session Replay for iOS (#4664)
1 parent 4e86db9 commit 6a67f5e

File tree

10 files changed

+194
-7
lines changed

10 files changed

+194
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633), [#4653](https://github.com/getsentry/sentry-dotnet/pull/4653))
1616
- The SDK now provides a `IsSessionActive` to allow checking the session state ([#4662](https://github.com/getsentry/sentry-dotnet/pull/4662))
1717
- The SDK now makes use of the new SessionEndStatus `Unhandled` when capturing an unhandled but non-terminal exception, i.e. through the UnobservedTaskExceptionIntegration ([#4633](https://github.com/getsentry/sentry-dotnet/pull/4633))
18+
- Added experimental support for Session Replay on iOS ([#4664](https://github.com/getsentry/sentry-dotnet/pull/4664))
1819
- Extended the App context by `app_memory` that can hold the amount of memory used by the application in bytes. ([#4707](https://github.com/getsentry/sentry-dotnet/pull/4707))
1920

2021
### Fixes

Sentry.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{A8A9
221221
scripts\build-sentry-cocoa.sh = scripts\build-sentry-cocoa.sh
222222
scripts\update-project-xml.ps1 = scripts\update-project-xml.ps1
223223
scripts\build-sentry-native.ps1 = scripts\build-sentry-native.ps1
224-
scripts\ios-simulator-utils.ps1 = scripts\ios-simulator-utils.ps1
225224
scripts\commit-formatted-code.sh = scripts\commit-formatted-code.sh
226225
scripts\accept-verifier-changes.ps1 = scripts\accept-verifier-changes.ps1
227226
scripts\generate-cocoa-bindings.ps1 = scripts\generate-cocoa-bindings.ps1

samples/Sentry.Samples.Maui/MauiProgram.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,22 @@ public static MauiApp CreateMauiApp()
4444
// Automatically create traces for async relay commands in the MVVM Community Toolkit
4545
options.AddCommunityToolkitIntegration();
4646

47-
#if __ANDROID__
48-
// Currently, experimental support is only available on Android
47+
#if __ANDROID__ || __IOS__ || __MACCATALYST__
48+
// Experimental support for Session Replay is currently available on Android, iOS and Mac Catalyst.
4949
options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate = 1.0;
5050
options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate = 1.0;
5151
// Mask all images and text by default. This can be overridden for individual view elements via the
5252
// sentry:SessionReplay.Mask XML attribute (see MainPage.xaml for an example)
5353
options.Native.ExperimentalOptions.SessionReplay.MaskAllImages = true;
5454
options.Native.ExperimentalOptions.SessionReplay.MaskAllText = true;
55+
#if __ANDROID__
5556
// Alternatively, the masking behaviour for entire classes of VisualElements can be configured here as
5657
// an exception to the default behaviour.
5758
// WARNING: In apps with complex user interfaces, consisting of hundreds of visual controls on a single
5859
// page, this option may cause performance issues. In such cases, consider applying the
5960
// sentry:SessionReplay.Mask="Unmask" attribute to individual controls instead.
6061
options.Native.ExperimentalOptions.SessionReplay.UnmaskControlsOfType<Button>();
62+
#endif
6163
#endif
6264

6365
options.SetBeforeScreenshotCapture((@event, hint) =>

samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
On Windows, we'll also build for Windows 10.
88
-->
99
<TargetFrameworks>$(TargetFrameworks);net9.0-android</TargetFrameworks>
10-
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0;net9.0-ios18.0;net9.0-maccatalyst18.0</TargetFrameworks>
10+
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
1111
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios18.0;net9.0-maccatalyst18.0</TargetFrameworks>
1212
<OutputType>Exe</OutputType>
1313
<RootNamespace>Sentry.Samples.Maui</RootNamespace>

scripts/patch-cocoa-bindings.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"SentryDsn",
115115
"SentryEvent",
116116
"SentryException",
117+
"SentryExperimentalOptions",
117118
"SentryFeedback",
118119
"SentryFeedbackAPI",
119120
"SentryFrame",
@@ -155,7 +156,10 @@
155156
"SentryUser",
156157
"SentryViewScreenshotOptions",
157158
"SentryViewScreenshotProvider"
158-
);
159+
)
160+
// Rename and retarget the experimental options property
161+
.RenameProperty("SentryOptions", "_swiftExperimentalOptions", "Experimental")
162+
.ChangePropertyType("SentryOptions", "Experimental", "SentryExperimentalOptions");
159163

160164
var formatted = CodeFormatter.Format(nodes, new AdhocWorkspace());
161165
File.WriteAllText(args[0], formatted.ToFullString() + "\n");
@@ -388,6 +392,31 @@ public static CompilationUnitSyntax VerifyProperty(
388392
.Where(node => node.Identifier.Matches(property) && node.HasParent(type));
389393
return root.ReplaceNodes(nodes, (node, _) => node.WithAttributeLists(node.AttributeLists.RemoveAttribute("Verify", verify)));
390394
}
395+
396+
public static CompilationUnitSyntax RenameProperty(
397+
this CompilationUnitSyntax root,
398+
string type,
399+
string from,
400+
string to)
401+
{
402+
var nodes = root.DescendantNodes()
403+
.OfType<PropertyDeclarationSyntax>()
404+
.Where(node => node.Identifier.Matches(from) && node.HasParent(type));
405+
return root.ReplaceNodes(nodes, (node, _) => node.WithIdentifier(SyntaxFactory.Identifier(to)));
406+
}
407+
408+
public static CompilationUnitSyntax ChangePropertyType(
409+
this CompilationUnitSyntax root,
410+
string type,
411+
string property,
412+
string newType)
413+
{
414+
var nodes = root.DescendantNodes()
415+
.OfType<PropertyDeclarationSyntax>()
416+
.Where(node => node.Identifier.Matches(property) && node.HasParent(type));
417+
return root.ReplaceNodes(nodes, (node, _) => node.WithType(SyntaxFactory.ParseTypeName(newType)));
418+
}
419+
391420
}
392421

393422
internal static class SyntaxNodeExtensions

src/Sentry.Bindings.Cocoa/ApiDefinitions.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1611,7 +1611,7 @@ interface SentryOptions
16111611

16121612
// @property (readonly, nonatomic) NSObject * _Nonnull _swiftExperimentalOptions;
16131613
[Export("_swiftExperimentalOptions")]
1614-
NSObject _swiftExperimentalOptions { get; }
1614+
SentryExperimentalOptions Experimental { get; }
16151615
}
16161616

16171617
// typedef void (^SentryProfilingConfigurationBlock)(SentryProfileOptions * _Nonnull);
@@ -2126,6 +2126,36 @@ interface SentryUser : SentrySerializable
21262126
nuint Hash { get; }
21272127
}
21282128

2129+
// @interface SentryExperimentalOptions : NSObject
2130+
[BaseType(typeof(NSObject), Name = "_TtC6Sentry25SentryExperimentalOptions")]
2131+
[Internal]
2132+
interface SentryExperimentalOptions
2133+
{
2134+
// @property (nonatomic) BOOL enableDataSwizzling;
2135+
[Export("enableDataSwizzling")]
2136+
bool EnableDataSwizzling { get; set; }
2137+
2138+
// @property (nonatomic) BOOL enableFileManagerSwizzling;
2139+
[Export("enableFileManagerSwizzling")]
2140+
bool EnableFileManagerSwizzling { get; set; }
2141+
2142+
// @property (nonatomic) BOOL enableUnhandledCPPExceptionsV2;
2143+
[Export("enableUnhandledCPPExceptionsV2")]
2144+
bool EnableUnhandledCPPExceptionsV2 { get; set; }
2145+
2146+
// @property (nonatomic) BOOL enableSessionReplayInUnreliableEnvironment;
2147+
[Export("enableSessionReplayInUnreliableEnvironment")]
2148+
bool EnableSessionReplayInUnreliableEnvironment { get; set; }
2149+
2150+
// @property (nonatomic) BOOL enableLogs;
2151+
[Export("enableLogs")]
2152+
bool EnableLogs { get; set; }
2153+
2154+
// -(void)validateOptions:(NSDictionary<NSString *,id> * _Nullable)options;
2155+
[Export("validateOptions:")]
2156+
void ValidateOptions([NullAllowed] NSDictionary<NSString, NSObject> options);
2157+
}
2158+
21292159
// @interface SentryFeedback : NSObject
21302160
[BaseType(typeof(NSObject), Name = "_TtC6Sentry14SentryFeedback")]
21312161
[DisableDefaultCtor]

src/Sentry/Internal/ReplaySession.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public SentryId? ActiveReplayId
2525
// Check to see if a Replay ID is available
2626
var replayId = JavaSdk.ScopesAdapter.Instance?.Options?.ReplayController?.ReplayId?.ToSentryId();
2727
return (replayId is { } id && id != SentryId.Empty) ? id : null;
28+
#elif __IOS__
29+
string? nativeId = null;
30+
SentryCocoaSdk.ConfigureScope(scope => nativeId = scope.ReplayId);
31+
return (nativeId is { } id) ? SentryId.Parse(id) : null;
2832
#else
2933
return null;
3034
#endif

src/Sentry/Platforms/Cocoa/SentryOptions.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,104 @@ public void AddInAppInclude(string prefix)
267267
InAppIncludes ??= new List<string>();
268268
InAppIncludes.Add(prefix);
269269
}
270+
271+
/// <summary>
272+
/// Options for experimental features in the native Sentry Cocoa SDK.
273+
/// </summary>
274+
public class NativeExperimentalOptions
275+
{
276+
/// <summary>
277+
/// Session Replay options.
278+
/// </summary>
279+
public NativeSentryReplayOptions SessionReplay { get; set; } = new();
280+
}
281+
282+
/// <summary>
283+
/// Session Replay options for the native Sentry Cocoa SDK.
284+
/// </summary>
285+
public class NativeSentryReplayOptions
286+
{
287+
/// <summary>
288+
/// <para>
289+
/// Forces enabling of session replay in unreliable environments.
290+
/// </para>
291+
/// <para>
292+
/// Due to internal changes with the release of Liquid Glass on iOS 26.0, the masking of text and images can
293+
/// not be reliably guaranteed. Therefore the SDK uses a defensive programming approach to disable the
294+
/// session replay integration by default, unless the environment is detected as reliable.
295+
/// </para>
296+
/// <para>
297+
/// Indicators for reliable environments include:
298+
/// <list type="bullet">
299+
/// <item>
300+
/// <description>Running on an older version of iOS that doesn't have Liquid Glass (iOS 18 or earlier)</description>
301+
/// </item>
302+
/// <item>
303+
/// <description><c>UIDesignRequiresCompatibility</c> is explicitly set to <c>YES</c> in <c>Info.plist</c></description>
304+
/// </item>
305+
/// <item>
306+
/// <description>The app was built with Xcode &lt; 26.0 (DTXcode &lt; 2600)</description>
307+
/// </item>
308+
/// </list>
309+
/// </para>
310+
/// <para>
311+
/// Important: This flag allows to re-enable the session replay integration on iOS 26.0 and later, but please be aware that text and images may not be masked as expected.
312+
/// </para>
313+
/// </summary>
314+
/// <remarks>
315+
/// See https://github.com/getsentry/sentry-cocoa/issues/6389
316+
/// </remarks>
317+
public bool EnableSessionReplayInUnreliableEnvironment { get; set; } = false;
318+
319+
/// <summary>
320+
/// The sample rate for sessions that had an error or crash.
321+
/// Value must be between 0.0 and 1.0.
322+
/// A value of 0.0 disables session replay for errored sessions.
323+
/// A value of 1.0 captures session replay for all errored sessions.
324+
/// </summary>
325+
public double? OnErrorSampleRate { get; set; }
326+
/// <summary>
327+
/// The sample rate for all sessions.
328+
/// Value must be between 0.0 and 1.0.
329+
/// A value of 0.0 disables session replay for all sessions.
330+
/// A value of 1.0 captures session replay for all sessions.
331+
/// </summary>
332+
public double? SessionSampleRate { get; set; }
333+
/// <summary>
334+
/// Whether to mask all images in the session replay by default.
335+
/// </summary>
336+
public bool MaskAllImages { get; set; } = true;
337+
/// <summary>
338+
/// Whether to mask all text in the session replay by default.
339+
/// </summary>
340+
public bool MaskAllText { get; set; } = true;
341+
342+
/// <summary>
343+
/// When enabled, reduces the impact of Session Replay on the main thread and potential frame drops. This is
344+
/// the default and recommended setting, but if you are experiencing issues then you can opt out by setting
345+
/// to <c>false</c>.
346+
/// </summary>
347+
/// <remarks>Defaults to <c>true</c></remarks>
348+
public bool EnableViewRendererV2 { get; set; } = true;
349+
350+
/// <summary>
351+
/// <para>
352+
/// Enables faster rendering of views at the cost of some visual fidelity.
353+
/// </para>
354+
/// <para>
355+
/// See: https://blog.sentry.io/boosting-session-replay-performance-on-ios-with-view-renderer-v2/
356+
/// </para>
357+
/// </summary>
358+
/// <remarks>Defaults to <c>false</c></remarks>
359+
public bool EnableFastViewRendering { get; set; } = false;
360+
361+
internal bool IsSessionReplayEnabled => OnErrorSampleRate > 0.0 || SessionSampleRate > 0.0;
362+
}
363+
364+
/// <summary>
365+
/// ExperimentalOptions
366+
/// </summary>
367+
public NativeExperimentalOptions ExperimentalOptions { get; set; } = new();
270368
}
271369

272370
// We actually add the profiling integration automatically in InitSentryCocoaSdk().

src/Sentry/Platforms/Cocoa/SentrySdk.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,29 @@ private static void InitSentryCocoaSdk(SentryOptions options)
145145
// nativeOptions.DefaultIntegrations
146146
// nativeOptions.EnableProfiling (deprecated)
147147

148+
// Session Replay options for the Cocoa SDK
149+
if (options.Native.ExperimentalOptions.SessionReplay.IsSessionReplayEnabled)
150+
{
151+
// For replay to work on iOS, session tracking must be enabled in the Cocoa SDK
152+
options.AutoSessionTracking = false;
153+
nativeOptions.EnableAutoSessionTracking = true;
154+
155+
// SDK users must explicitly opt-in to Session Replay in unreliable environments
156+
nativeOptions.Experimental.EnableSessionReplayInUnreliableEnvironment =
157+
options.Native.ExperimentalOptions.SessionReplay.EnableSessionReplayInUnreliableEnvironment;
158+
159+
var sessionSampleRate = (float)(options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate ?? 0f);
160+
var onErrorSampleRate = (float)(options.Native.ExperimentalOptions.SessionReplay.OnErrorSampleRate ?? 0f);
161+
var cocoaReplayOptions = new Sentry.CocoaSdk.SentryReplayOptions();
162+
cocoaReplayOptions.SessionSampleRate = sessionSampleRate;
163+
cocoaReplayOptions.OnErrorSampleRate = onErrorSampleRate;
164+
cocoaReplayOptions.MaskAllText = options.Native.ExperimentalOptions.SessionReplay.MaskAllText;
165+
cocoaReplayOptions.MaskAllImages = options.Native.ExperimentalOptions.SessionReplay.MaskAllImages;
166+
cocoaReplayOptions.EnableViewRendererV2 = options.Native.ExperimentalOptions.SessionReplay.EnableViewRendererV2;
167+
cocoaReplayOptions.EnableFastViewRendering = options.Native.ExperimentalOptions.SessionReplay.EnableFastViewRendering;
168+
nativeOptions.SessionReplay = cocoaReplayOptions;
169+
}
170+
148171
// Set hybrid SDK name
149172
SentryCocoaHybridSdk.SetSdkName("sentry.cocoa.dotnet");
150173

test/Sentry.Tests/Platforms/iOS/BindableSentryOptionsTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ namespace Sentry.Tests.Platforms.Cocoa;
66
public class BindableSentryOptionsTests : BindableTests<SentryOptions.NativeOptions>
77
{
88
public BindableSentryOptionsTests()
9-
: base(nameof(SentryOptions.NativeOptions.UrlSessionDelegate))
9+
: base(nameof(SentryOptions.NativeOptions.UrlSessionDelegate),
10+
nameof(SentryOptions.NativeOptions.ExperimentalOptions))
1011
{
1112
}
1213

0 commit comments

Comments
 (0)