Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ 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 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);
}

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

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
7 changes: 7 additions & 0 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 @@ -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);

Expand Down
12 changes: 11 additions & 1 deletion test/Sentry.Tests/SentryHttpMessageHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ public void Send_Executed_BreadcrumbCreated()
}
#endif

#if ANDROID || IOS || MACCATALYST
[Fact]
public void HandleResponse_SpanExists_AddsReplayBreadcrumbData()
{
Expand Down Expand Up @@ -644,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 @@ -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
}
Loading