Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

- Add _experimental_ support for [Sentry trace-connected Metrics](https://docs.sentry.io/product/explore/metrics/) ([#4834](https://github.com/getsentry/sentry-dotnet/pull/4834))
- Extended `SentryThread` by `Main` to allow indication whether the thread is considered the current main thread ([#4807](https://github.com/getsentry/sentry-dotnet/pull/4807))
- Outbound HTTP requests now show in the Network tab for Android Session Replays ([#4860](https://github.com/getsentry/sentry-dotnet/pull/4860))

### Fixes

Expand Down
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
31 changes: 31 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,37 @@
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 NSDictionary<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[key] = dto.ToNSDate();
continue;
}

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

Check failure on line 220 in src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs

View check run for this annotation

@sentry/warden / warden: code-review

NSDictionary indexer assignment will fail at runtime - NSDictionary is immutable

The code creates an `NSDictionary<NSString, NSObject>` on line 207 and then tries to assign values using the indexer (`dict[key] = value`) on lines 216 and 220. In .NET iOS/Mac bindings, `NSDictionary` is immutable and does not support indexer assignment - this will throw a runtime exception. The existing pattern in this file (see `ToNSDictionary()` at lines 166-169) correctly uses an intermediate .NET `Dictionary<>` and then converts using `NSDictionary.FromObjectsAndKeys()`.
}

return dict.Count == 0 ? null : dict;

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

Check failure on line 227 in src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs

View check run for this annotation

@sentry/warden / warden: find-bugs

ToCocoaBreadcrumbData is never called, iOS Session Replay network details will not work

The new `ToCocoaBreadcrumbData` method is created to convert `request_start` to `NSDate` for iOS Session Replay (as referenced in the comment linking to sentry-cocoa), but it is never wired up. The `BreadcrumbExtensions.ToCocoaBreadcrumb` method (in BreadcrumbExtensions.cs line 22) still calls `ToNullableNSDictionary()` instead of the new `ToCocoaBreadcrumbData()`. This means HTTP breadcrumb data will continue to store `request_start` as a string instead of an `NSDate`, and the network tab in iOS Session Replay will not show the outbound HTTP requests as intended by this PR.

/// <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
75 changes: 67 additions & 8 deletions test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -611,25 +611,84 @@
Assert.True(breadcrumbGenerated.Data.ContainsKey(statusKey));
Assert.Equal(expectedBreadcrumbData[statusKey], breadcrumbGenerated.Data[statusKey]);
}
#endif

#if ANDROID || IOS || MACCATALYST
[Fact]
public void Send_Executed_FailedRequestsCaptured()
public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
{
// Arrange
var scope = new Scope();
var hub = Substitute.For<IHub>();
var failedRequestHandler = Substitute.For<ISentryFailedRequestHandler>();
var options = new SentryOptions();
hub.SubstituteConfigureScope(scope);

var options = new SentryOptions
{
CaptureFailedRequests = false
};

var sut = new SentryHttpMessageHandler(hub, options);

var method = "GET";
var url = "https://localhost/";
var response = new HttpResponseMessage(HttpStatusCode.OK);

using var innerHandler = new FakeHttpMessageHandler();
using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler);
using var client = new HttpClient(sentryHandler);
var span = Substitute.For<ISpan>();
span.StartTimestamp.Returns(DateTimeOffset.UtcNow.AddMilliseconds(-50));

// Act
client.Get(url);
sut.HandleResponse(response, span, method, url);

// Assert
failedRequestHandler.Received(1).HandleResponse(Arg.Any<HttpResponseMessage>());
var breadcrumb = scope.Breadcrumbs.First();
breadcrumb.Type.Should().Be("http");
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
}

Check failure on line 667 in test/Sentry.Tests/SentryHttpMessageHandlerTests.cs

View check run for this annotation

@sentry/warden / warden: find-bugs

Duplicate test methods will cause compiler error on Android

The new test methods `HandleResponse_SpanExists_AddsReplayBreadcrumbData` and `HandleResponse_NoSpanExists_NoReplayBreadcrumbData` are defined inside `#if ANDROID || IOS || MACCATALYST` block (lines 617-692). However, identical method names already exist in the `#if ANDROID` block (lines 696-765). When building for Android, both conditional blocks are active, resulting in duplicate method definitions that will fail compilation.

[Fact]
public void HandleResponse_NoSpanExists_NoReplayBreadcrumbData()
{
// Arrange
var scope = new Scope();
var hub = Substitute.For<IHub>();
hub.SubstituteConfigureScope(scope);

var sut = new SentryHttpMessageHandler(hub, null);

var method = "GET";
var url = "https://localhost/";
var response = new HttpResponseMessage(HttpStatusCode.OK);

// Act
sut.HandleResponse(response, span: null, method, url);

// Assert
var breadcrumb = scope.Breadcrumbs.First();
breadcrumb.Data.Should().NotBeNull();
breadcrumb.Data!.Should().NotContainKey(SentryHttpMessageHandler.HttpStartTimestampKey);
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.HttpEndTimestampKey);
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.RequestStartKey);
}

Check failure on line 692 in test/Sentry.Tests/SentryHttpMessageHandlerTests.cs

View check run for this annotation

@sentry/warden / warden: code-review

Duplicate test method definitions will cause compile error on Android

The test methods `HandleResponse_SpanExists_AddsReplayBreadcrumbData` and `HandleResponse_NoSpanExists_NoReplayBreadcrumbData` are defined twice: once in the `#if ANDROID || IOS || MACCATALYST` block (lines 618-692) and again in the `#if ANDROID` block (lines 697-765 in context after). When compiling for ANDROID, both preprocessor conditions are true, resulting in duplicate method definitions that will cause a compilation error.

#endif
Expand Down
Loading