Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0289e93
Initial commit
jamescrosswell Jan 22, 2026
588beb3
.
jamescrosswell Jan 22, 2026
31fd5a0
Format code
getsentry-bot Jan 22, 2026
37254e4
Allow building against local instance of Sentry Java SDK
jamescrosswell Jan 22, 2026
79e9225
Fixed local maven references
jamescrosswell Jan 26, 2026
cabaa59
Fix issues building against local maven repository
jamescrosswell Jan 26, 2026
4cdf36c
Remove unecessary assignment of default converter
jamescrosswell Jan 27, 2026
d05cde3
Format code
getsentry-bot Jan 27, 2026
0b6c66c
Adding sample code temporarily
jamescrosswell Jan 27, 2026
96e9e26
Added custom breadcrumb converter to deal with the conversion between…
jamescrosswell Jan 28, 2026
c3a3889
Remove unecessary changes refactored to other PRs
jamescrosswell Jan 28, 2026
32b1b26
Remove changes refactored to #4873
jamescrosswell Jan 28, 2026
a40a7a0
Consolidate constants
jamescrosswell Jan 28, 2026
85d47f7
message handler tests
jamescrosswell Jan 28, 2026
e51db44
fixed test
jamescrosswell Jan 28, 2026
2c847fe
Added DotnetReplayBreadcrumbConverterTests
jamescrosswell Jan 29, 2026
6da450a
changelog
jamescrosswell Jan 29, 2026
e7b460b
Merge remote-tracking branch 'origin/main' into replay-network-spans
jamescrosswell Feb 1, 2026
9622e62
feat: Add network details for session replay on iOS
jamescrosswell Feb 3, 2026
b49f55d
Merge branch 'main' into ios-performance-spans
jamescrosswell Mar 17, 2026
b632782
Remove erroneous changelog entry
jamescrosswell Mar 17, 2026
b389d93
Merge remote-tracking branch 'origin/main' into ios-performance-spans
jamescrosswell Mar 22, 2026
d115e5c
Fix merge errors
jamescrosswell Mar 23, 2026
4ec6bae
Review feedback
jamescrosswell Mar 23, 2026
bb33532
Fix sample for iOS 26.0
jamescrosswell Mar 23, 2026
eb1828c
Added tests
jamescrosswell Mar 23, 2026
70ac898
Fix bug from seer review
jamescrosswell Mar 25, 2026
9e3f5f8
Make comment in sample more explicit
jamescrosswell Mar 25, 2026
43fd75b
Merge remote-tracking branch 'origin/main' into ios-performance-spans
jamescrosswell Mar 25, 2026
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
1 change: 1 addition & 0 deletions samples/Sentry.Samples.Maui/MainPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Globalization;
using Microsoft.Extensions.Logging;

namespace Sentry.Samples.Maui;
Expand Down
5 changes: 5 additions & 0 deletions samples/Sentry.Samples.Maui/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public static MauiApp CreateMauiApp()
// sentry:SessionReplay.Mask="Unmask" attribute to individual controls instead.
options.Native.ExperimentalOptions.SessionReplay.UnmaskControlsOfType<Button>();
#endif
#if __IOS__ || __MACCATALYST__
// SDK users must explicitly opt-in to Session Replay in unreliable environments - when running liquid
// glass on iOS 26.0 or later
options.Native.ExperimentalOptions.SessionReplay.EnableSessionReplayInUnreliableEnvironment = true;
#endif
#endif

options.SetBeforeScreenshotCapture((@event, hint) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static CocoaSdk.SentryBreadcrumb ToCocoaBreadcrumb(this Breadcrumb breadc
Timestamp = breadcrumb.Timestamp.ToNSDate(),
Message = breadcrumb.Message,
Type = breadcrumb.Type,
Data = breadcrumb.Data?.ToNullableNSDictionary(),
Data = breadcrumb.Data?.ToCocoaBreadcrumbData(),
Category = breadcrumb.Category ?? "",
Level = breadcrumb.Level.ToCocoaSentryLevel()
};
Expand Down
33 changes: 33 additions & 0 deletions src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,39 @@ public static NSDictionary<NSString, NSString> ToNSDictionaryStrings(
this IReadOnlyCollection<KeyValuePair<string, TValue>> dict) =>
dict.Count == 0 ? null : dict.ToNSDictionary();

public static NSDictionary<NSString, NSObject>? ToCocoaBreadcrumbData(
this IReadOnlyDictionary<string, string> source)
{
// Avoid an allocation if we can
if (source.Count == 0)
{
return null;
}

var dict = new Dictionary<NSString, NSObject>();

foreach (var (key, value) in source)
{
// Cocoa Session Replay expects `request_start` to be a Date (`NSDate`).
// See https://github.com/getsentry/sentry-cocoa/blob/2b4e787e55558e1475eda8f98b02c19a0d511741/Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift#L73
if (key == SentryHttpMessageHandler.RequestStartKey && TryParseUnixMs(value, out var unixMs))
{
var dto = DateTimeOffset.FromUnixTimeMilliseconds(unixMs);
dict[(NSString)key] = dto.ToNSDate();
continue;
}

dict[(NSString)key] = NSObject.FromObject(value);
}

return dict.Count == 0
? null
: NSDictionary<NSString, NSObject>.FromObjectsAndKeys(dict.Values.ToArray(), dict.Keys.ToArray());

static bool TryParseUnixMs(string value, out long unixMs) =>
long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out unixMs);
}

/// <summary>
/// Converts an <see cref="NSNumber"/> to a .NET primitive data type and returns the result box in an <see cref="object"/>.
/// </summary>
Expand Down
9 changes: 7 additions & 2 deletions src/Sentry/SentryHttpMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class SentryHttpMessageHandler : SentryMessageHandler
internal const string HttpClientOrigin = "auto.http.client";
internal const string HttpStartTimestampKey = "http.start_timestamp";
internal const string HttpEndTimestampKey = "http.end_timestamp";
internal const string RequestStartKey = "request_start";

/// <summary>
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
Expand Down Expand Up @@ -91,15 +92,19 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS
{"method", method},
{"status_code", ((int) response.StatusCode).ToString()}
};
#if ANDROID
if (span is not null)
{
#if ANDROID
// Ensure the breadcrumb can be converted to RRWeb so that it shows up in the network tab in Session Replay.
// See https://github.com/getsentry/sentry-java/blob/94bff8dc0a952ad8c1b6815a9eda5005e41b92c7/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt#L195-L199
breadcrumbData[HttpStartTimestampKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
breadcrumbData[HttpEndTimestampKey] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
}
#elif IOS || MACCATALYST
// Ensure the breadcrumb can be converted to RRWeb so that it shows up in the network tab in Session Replay.
// See https://github.com/getsentry/sentry-cocoa/blob/2b4e787e55558e1475eda8f98b02c19a0d511741/Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift#L70-L86
breadcrumbData[RequestStartKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
#endif
}
_hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);

// Create events for failed requests
Expand Down
58 changes: 58 additions & 0 deletions test/Sentry.Tests/Platforms/iOS/CocoaExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using FluentAssertions;
using Foundation;
Expand Down Expand Up @@ -78,6 +79,63 @@ public void ToSentryEvent_ConvertToManaged()
AssertEqual(managed, native);
}

[Fact]
public void ToCocoaBreadcrumbData_EmptyDictionary_ReturnsNull()
{
var source = new Dictionary<string, string>();
var result = source.ToCocoaBreadcrumbData();
result.Should().BeNull();
}

[Fact]
public void ToCocoaBreadcrumbData_RegularKeys_ConvertsToNSObject()
{
var source = new Dictionary<string, string>
{
{ "url", "https://example.com" },
{ "method", "GET" }
};

var result = source.ToCocoaBreadcrumbData();

result.Should().NotBeNull();
result!.Count.Should().Be(2);
result[(NSString)"url"].Should().Be(NSObject.FromObject("https://example.com"));
result[(NSString)"method"].Should().Be(NSObject.FromObject("GET"));
}

[Fact]
public void ToCocoaBreadcrumbData_RequestStartKey_ConvertsToNSDate()
{
var timestamp = DateTimeOffset.UtcNow;
var unixMs = timestamp.ToUnixTimeMilliseconds();
var source = new Dictionary<string, string>
{
{ SentryHttpMessageHandler.RequestStartKey, unixMs.ToString("F0", CultureInfo.InvariantCulture) }
};

var result = source.ToCocoaBreadcrumbData();

result.Should().NotBeNull();
result!.Count.Should().Be(1);
result[(NSString)SentryHttpMessageHandler.RequestStartKey].Should().BeOfType<NSDate>();
}

[Fact]
public void ToCocoaBreadcrumbData_RequestStartKey_NonNumericValue_FallsBackToNSObject()
{
var source = new Dictionary<string, string>
{
{ SentryHttpMessageHandler.RequestStartKey, "not-a-number" }
};

var result = source.ToCocoaBreadcrumbData();

result.Should().NotBeNull();
result!.Count.Should().Be(1);
result[(NSString)SentryHttpMessageHandler.RequestStartKey].Should().Be(NSObject.FromObject("not-a-number"));
}

private static void AssertEqual(SentryEvent managed, CocoaSdk.SentryEvent native)
{
native.ServerName.Should().Be(managed.ServerName, "Server Name");
Expand Down
33 changes: 10 additions & 23 deletions test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,30 +611,9 @@ public void Send_Executed_BreadcrumbCreated()
Assert.True(breadcrumbGenerated.Data.ContainsKey(statusKey));
Assert.Equal(expectedBreadcrumbData[statusKey], breadcrumbGenerated.Data[statusKey]);
}

[Fact]
public void Send_Executed_FailedRequestsCaptured()
{
// Arrange
var hub = Substitute.For<IHub>();
var failedRequestHandler = Substitute.For<ISentryFailedRequestHandler>();
var options = new SentryOptions();
var url = "https://localhost/";

using var innerHandler = new FakeHttpMessageHandler();
using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler);
using var client = new HttpClient(sentryHandler);

// Act
client.Get(url);

// Assert
failedRequestHandler.Received(1).HandleResponse(Arg.Any<HttpResponseMessage>());
}

#endif

#if ANDROID
#if ANDROID || IOS || MACCATALYST
[Fact]
public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
{
Expand Down Expand Up @@ -666,18 +645,25 @@ public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
breadcrumb.Category.Should().Be("http");

breadcrumb.Data.Should().NotBeNull();
#if ANDROID
breadcrumb.Data!.Should().ContainKey(SentryHttpMessageHandler.HttpStartTimestampKey);
breadcrumb.Data.Should().ContainKey(SentryHttpMessageHandler.HttpEndTimestampKey);

long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.HttpStartTimestampKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startMs)
.Should().BeTrue();
long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.HttpEndTimestampKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var endMs)
.Should().BeTrue();

startMs.Should().BeGreaterThan(0);
startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds());
endMs.Should().BeGreaterThan(0);
endMs.Should().BeGreaterOrEqualTo(startMs);
#elif IOS || MACCATALYST
breadcrumb.Data!.Should().ContainKey(SentryHttpMessageHandler.RequestStartKey);
long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.RequestStartKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startMs)
.Should().BeTrue();
startMs.Should().BeGreaterThan(0);
startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds());
#endif
}

[Fact]
Expand All @@ -702,6 +688,7 @@ public void HandleResponse_NoSpanExists_NoReplayBreadcrumbData()
breadcrumb.Data.Should().NotBeNull();
breadcrumb.Data!.Should().NotContainKey(SentryHttpMessageHandler.HttpStartTimestampKey);
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.HttpEndTimestampKey);
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.RequestStartKey);
}
#endif
}
Loading