diff --git a/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs b/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs index cf6776a450..05ae6911be 100644 --- a/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs +++ b/src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs @@ -195,6 +195,37 @@ public static NSDictionary ToNSDictionaryStrings( this IReadOnlyCollection> dict) => dict.Count == 0 ? null : dict.ToNSDictionary(); + public static NSDictionary? ToCocoaBreadcrumbData( + this IReadOnlyDictionary source) + { + // Avoid an allocation if we can + if (source.Count == 0) + { + return null; + } + + var dict = new NSDictionary(); + + 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); + } + + return dict.Count == 0 ? null : dict; + + static bool TryParseUnixMs(string value, out long unixMs) => + long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out unixMs); + } + /// /// Converts an to a .NET primitive data type and returns the result box in an . /// diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index 7792bbc2d6..c2aca78cdf 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -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"; /// /// Constructs an instance of . @@ -93,10 +94,16 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS }; 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); diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index d7edeff711..14009a6549 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -613,6 +613,7 @@ public void Send_Executed_BreadcrumbCreated() } #endif +#if ANDROID || IOS || MACCATALYST [Fact] public void HandleResponse_SpanExists_AddsReplayBreadcrumbData() { @@ -644,6 +645,7 @@ 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); @@ -651,11 +653,17 @@ public void HandleResponse_SpanExists_AddsReplayBreadcrumbData() .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] @@ -680,5 +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 }