Skip to content

Commit 9622e62

Browse files
feat: Add network details for session replay on iOS
1 parent e7b460b commit 9622e62

File tree

3 files changed

+49
-1
lines changed

3 files changed

+49
-1
lines changed

src/Sentry/Platforms/Cocoa/Extensions/CocoaExtensions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,37 @@ public static NSDictionary<NSString, NSString> ToNSDictionaryStrings(
195195
this IReadOnlyCollection<KeyValuePair<string, TValue>> dict) =>
196196
dict.Count == 0 ? null : dict.ToNSDictionary();
197197

198+
public static NSDictionary<NSString, NSObject>? ToCocoaBreadcrumbData(
199+
this IReadOnlyDictionary<string, string> source)
200+
{
201+
// Avoid an allocation if we can
202+
if (source.Count == 0)
203+
{
204+
return null;
205+
}
206+
207+
var dict = new NSDictionary<NSString, NSObject>();
208+
209+
foreach (var (key, value) in source)
210+
{
211+
// Cocoa Session Replay expects `request_start` to be a Date (`NSDate`).
212+
// See https://github.com/getsentry/sentry-cocoa/blob/2b4e787e55558e1475eda8f98b02c19a0d511741/Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift#L73
213+
if (key == SentryHttpMessageHandler.RequestStartKey && TryParseUnixMs(value, out var unixMs))
214+
{
215+
var dto = DateTimeOffset.FromUnixTimeMilliseconds(unixMs);
216+
dict[key] = dto.ToNSDate();
217+
continue;
218+
}
219+
220+
dict[key] = NSObject.FromObject(value);
221+
}
222+
223+
return dict.Count == 0 ? null : dict;
224+
225+
static bool TryParseUnixMs(string value, out long unixMs) =>
226+
long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out unixMs);
227+
}
228+
198229
/// <summary>
199230
/// Converts an <see cref="NSNumber"/> to a .NET primitive data type and returns the result box in an <see cref="object"/>.
200231
/// </summary>

src/Sentry/SentryHttpMessageHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class SentryHttpMessageHandler : SentryMessageHandler
1717
internal const string HttpClientOrigin = "auto.http.client";
1818
internal const string HttpStartTimestampKey = "http.start_timestamp";
1919
internal const string HttpEndTimestampKey = "http.end_timestamp";
20+
internal const string RequestStartKey = "request_start";
2021

2122
/// <summary>
2223
/// Constructs an instance of <see cref="SentryHttpMessageHandler"/>.
@@ -93,10 +94,16 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS
9394
};
9495
if (span is not null)
9596
{
97+
#if ANDROID
9698
// Ensure the breadcrumb can be converted to RRWeb so that it shows up in the network tab in Session Replay.
9799
// See https://github.com/getsentry/sentry-java/blob/94bff8dc0a952ad8c1b6815a9eda5005e41b92c7/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt#L195-L199
98100
breadcrumbData[HttpStartTimestampKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
99101
breadcrumbData[HttpEndTimestampKey] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
102+
#elif IOS || MACCATALYST
103+
// Ensure the breadcrumb can be converted to RRWeb so that it shows up in the network tab in Session Replay.
104+
// See https://github.com/getsentry/sentry-cocoa/blob/2b4e787e55558e1475eda8f98b02c19a0d511741/Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift#L70-L86
105+
breadcrumbData[RequestStartKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture);
106+
#endif
100107
}
101108
_hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData);
102109

test/Sentry.Tests/SentryHttpMessageHandlerTests.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ public void Send_Executed_BreadcrumbCreated()
613613
}
614614
#endif
615615

616+
#if ANDROID || IOS || MACCATALYST
616617
[Fact]
617618
public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
618619
{
@@ -644,18 +645,25 @@ public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
644645
breadcrumb.Category.Should().Be("http");
645646

646647
breadcrumb.Data.Should().NotBeNull();
648+
#if ANDROID
647649
breadcrumb.Data!.Should().ContainKey(SentryHttpMessageHandler.HttpStartTimestampKey);
648650
breadcrumb.Data.Should().ContainKey(SentryHttpMessageHandler.HttpEndTimestampKey);
649651

650652
long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.HttpStartTimestampKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startMs)
651653
.Should().BeTrue();
652654
long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.HttpEndTimestampKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var endMs)
653655
.Should().BeTrue();
654-
655656
startMs.Should().BeGreaterThan(0);
656657
startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds());
657658
endMs.Should().BeGreaterThan(0);
658659
endMs.Should().BeGreaterOrEqualTo(startMs);
660+
#elif IOS || MACCATALYST
661+
breadcrumb.Data!.Should().ContainKey(SentryHttpMessageHandler.RequestStartKey);
662+
long.TryParse(breadcrumb.Data![SentryHttpMessageHandler.RequestStartKey], NumberStyles.Integer, CultureInfo.InvariantCulture, out var startMs)
663+
.Should().BeTrue();
664+
startMs.Should().BeGreaterThan(0);
665+
startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds());
666+
#endif
659667
}
660668

661669
[Fact]
@@ -680,5 +688,7 @@ public void HandleResponse_NoSpanExists_NoReplayBreadcrumbData()
680688
breadcrumb.Data.Should().NotBeNull();
681689
breadcrumb.Data!.Should().NotContainKey(SentryHttpMessageHandler.HttpStartTimestampKey);
682690
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.HttpEndTimestampKey);
691+
breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.RequestStartKey);
683692
}
693+
#endif
684694
}

0 commit comments

Comments
 (0)