From 0289e935701a5b116ee1e071c409171502499a1b Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 22 Jan 2026 13:32:46 +1300 Subject: [PATCH 01/20] Initial commit --- .../DefaultReplayBreadcrumbConverter.cs | 12 ++++++++++++ src/Sentry.Bindings.Android/README.md | 2 +- src/Sentry.Bindings.Android/Transforms/Metadata.xml | 8 +++----- src/Sentry/Platforms/Android/SentrySdk.cs | 5 +++++ src/Sentry/SentryHttpMessageHandler.cs | 7 +++++++ 5 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs new file mode 100644 index 0000000000..bbb37a54d4 --- /dev/null +++ b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs @@ -0,0 +1,12 @@ +#if __ANDROID__ +using Sentry.JavaSdk; + +// ReSharper disable once CheckNamespace - match generated code namespace +namespace Sentry.JavaSdk.Android.Replay; + +// This partial augments the generated binding to implement the managed interface... to work arould +// a problem with source generators for the bindings +internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter +{ +} +#endif diff --git a/src/Sentry.Bindings.Android/README.md b/src/Sentry.Bindings.Android/README.md index 3981ca163f..4312b1b37e 100644 --- a/src/Sentry.Bindings.Android/README.md +++ b/src/Sentry.Bindings.Android/README.md @@ -6,7 +6,7 @@ Instead, reference one of the following: ## SDK Developers and Contributors -For .NET SDK contributors, most of the classes in this package are generated automatically. +For .NET SDK contributors, most of the classes in this package are generated automatically. These can be found in the `src/Sentry.Bindings.Android/obj/Debug/{tfm}/generated/src/` folder after building the project. - Post generation transformations are controlled via various XML files stored in the `/Transforms` directory (see [Java Bindings Metadata documentation](https://learn.microsoft.com/en-gb/previous-versions/xamarin/android/platform/binding-java-library/customizing-bindings/java-bindings-metadata) for details). diff --git a/src/Sentry.Bindings.Android/Transforms/Metadata.xml b/src/Sentry.Bindings.Android/Transforms/Metadata.xml index 74d65333e8..426f41579b 100644 --- a/src/Sentry.Bindings.Android/Transforms/Metadata.xml +++ b/src/Sentry.Bindings.Android/Transforms/Metadata.xml @@ -24,6 +24,7 @@ Sentry.JavaSdk.Android.Core.Internal.ThreadDump Sentry.JavaSdk.Android.Core.Internal.Util Sentry.JavaSdk.Android.Ndk + Sentry.JavaSdk.Android.Replay Sentry.JavaSdk.Android.Supplemental Sentry.JavaSdk.Cache @@ -72,7 +73,7 @@ - +x @@ -111,7 +112,7 @@ --> - + - - - diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 6b92b601ec..0341568ab3 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -144,6 +144,11 @@ private static void InitSentryAndroidSdk(SentryOptions options) (JavaDouble?)options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate; o.SessionReplay.SetMaskAllImages(options.Native.ExperimentalOptions.SessionReplay.MaskAllImages); o.SessionReplay.SetMaskAllText(options.Native.ExperimentalOptions.SessionReplay.MaskAllText); + // Set the BreadcrumbConverter to the DefaultReplayBreadcrumbConverter + if (o.ReplayController is {} replayController) + { + replayController.BreadcrumbConverter = new Sentry.JavaSdk.Android.Replay.DefaultReplayBreadcrumbConverter(o); + } // These options are intentionally set and not exposed for modification o.EnableExternalConfiguration = false; diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index f75f958b8a..863164e9d0 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -89,6 +89,13 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS {"method", method}, {"status_code", ((int) response.StatusCode).ToString()} }; + if (span is not null) + { + // 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["http.start_timestamp"] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); + breadcrumbData["http.end_timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); + } _hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData); // Create events for failed requests From 588beb3f10ffa434f0114665ec25257c9daf6959 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 22 Jan 2026 13:59:35 +1300 Subject: [PATCH 02/20] . --- src/Sentry.Bindings.Android/Transforms/Metadata.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.Bindings.Android/Transforms/Metadata.xml b/src/Sentry.Bindings.Android/Transforms/Metadata.xml index 426f41579b..1a0c8bb6e9 100644 --- a/src/Sentry.Bindings.Android/Transforms/Metadata.xml +++ b/src/Sentry.Bindings.Android/Transforms/Metadata.xml @@ -73,7 +73,7 @@ -x + From 31fd5a03dc0d8322c945aa97dc7482d7d6afa72e Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 22 Jan 2026 01:18:25 +0000 Subject: [PATCH 03/20] Format code --- src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs | 2 +- src/Sentry/Platforms/Android/SentrySdk.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs index bbb37a54d4..35139fbbb2 100644 --- a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs +++ b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs @@ -6,7 +6,7 @@ namespace Sentry.JavaSdk.Android.Replay; // This partial augments the generated binding to implement the managed interface... to work arould // a problem with source generators for the bindings -internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter +internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter { } #endif diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 0341568ab3..90b69552e5 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -145,7 +145,7 @@ private static void InitSentryAndroidSdk(SentryOptions options) o.SessionReplay.SetMaskAllImages(options.Native.ExperimentalOptions.SessionReplay.MaskAllImages); o.SessionReplay.SetMaskAllText(options.Native.ExperimentalOptions.SessionReplay.MaskAllText); // Set the BreadcrumbConverter to the DefaultReplayBreadcrumbConverter - if (o.ReplayController is {} replayController) + if (o.ReplayController is { } replayController) { replayController.BreadcrumbConverter = new Sentry.JavaSdk.Android.Replay.DefaultReplayBreadcrumbConverter(o); } From 37254e421398d54d9fd6150ef8d08c256afddb5e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 23 Jan 2026 11:06:49 +1300 Subject: [PATCH 04/20] Allow building against local instance of Sentry Java SDK --- .../Sentry.Bindings.Android.csproj | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index 3da14848ad..64605d351c 100644 --- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj +++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj @@ -7,6 +7,23 @@ .NET Bindings for the Sentry Android SDK + + + true + + + $(LocalSentryJavaRepoDir)sentry/build/libs/sentry-$(SentryAndroidSdkVersion).jar + $(LocalSentryJavaRepoDir)sentry-android-core/build/outputs/aar/sentry-android-core-debug.aar + $(LocalSentryJavaRepoDir)sentry-android-ndk/build/outputs/aar/sentry-android-ndk-debug.aar + $(LocalSentryJavaRepoDir)sentry-android-replay/build/outputs/aar/sentry-android-replay-debug.aar + @@ -73,13 +90,21 @@ - + + + + + + + + + @@ -88,7 +113,7 @@ - + Date: Mon, 26 Jan 2026 16:32:07 +1300 Subject: [PATCH 05/20] Fixed local maven references --- .../DefaultReplayBreadcrumbConverter.cs | 8 +-- .../Sentry.Bindings.Android.csproj | 67 +++++++++++++------ 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs index 35139fbbb2..252f059a3c 100644 --- a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs +++ b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs @@ -4,9 +4,7 @@ // ReSharper disable once CheckNamespace - match generated code namespace namespace Sentry.JavaSdk.Android.Replay; -// This partial augments the generated binding to implement the managed interface... to work arould -// a problem with source generators for the bindings -internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter -{ -} +// This file used to try to "patch" the generated bindings via a partial class. +// It caused build breaks when the generated Java binding shape changed. +// Binding fixes should be done via Transforms/Metadata.xml instead. #endif diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index 64605d351c..a0596b454e 100644 --- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj +++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj @@ -3,26 +3,32 @@ $(LatestAndroidTfm);$(PreviousAndroidTfm) 8.29.0 $(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\ - + .NET Bindings for the Sentry Android SDK - - true + To populate MavenLocal from your checkout: + cd $(LocalSentryJavaRepoDir) && ./gradlew publishToMavenLocal + cd $(LocalSentryNativeRepoDir)/ndk && ./gradlew publishToMavenLocal - - $(LocalSentryJavaRepoDir)sentry/build/libs/sentry-$(SentryAndroidSdkVersion).jar - $(LocalSentryJavaRepoDir)sentry-android-core/build/outputs/aar/sentry-android-core-debug.aar - $(LocalSentryJavaRepoDir)sentry-android-ndk/build/outputs/aar/sentry-android-ndk-debug.aar - $(LocalSentryJavaRepoDir)sentry-android-replay/build/outputs/aar/sentry-android-replay-debug.aar + Note that you may need to check out specific branches/tags to match the version defined in SentryAndroidSdkVersion + and the resulting native ndk version. + --> + false + $(HOME)/.m2/repository/ + $(LocalSentryMavenRepoDir)io/sentry/sentry/$(SentryAndroidSdkVersion)/sentry-$(SentryAndroidSdkVersion).jar + $(LocalSentryMavenRepoDir)io/sentry/sentry-android-core/$(SentryAndroidSdkVersion)/sentry-android-core-$(SentryAndroidSdkVersion).aar + $(LocalSentryMavenRepoDir)io/sentry/sentry-android-ndk/$(SentryAndroidSdkVersion)/sentry-android-ndk-$(SentryAndroidSdkVersion).aar + $(LocalSentryMavenRepoDir)io/sentry/sentry-android-replay/$(SentryAndroidSdkVersion)/sentry-android-replay-$(SentryAndroidSdkVersion).aar + $([System.String]::Copy($(LocalSentryJarPath)).Replace('.jar', '.pom')) + $([System.String]::Copy($(LocalSentryAndroidCoreAarPath)).Replace('.aar', '.pom')) + $([System.String]::Copy($(LocalSentryAndroidNdkAarPath)).Replace('.aar', '.pom')) + $([System.String]::Copy($(LocalSentryAndroidReplayAarPath)).Replace('.aar', '.pom')) @@ -90,7 +96,7 @@ - + @@ -98,11 +104,15 @@ - - - - - + + + + + @@ -113,7 +123,7 @@ - + + + + <_LocalAndroidNdkPom>$(LocalSentryMavenRepoDir)io/sentry/sentry-android-ndk/$(SentryAndroidSdkVersion)/sentry-android-ndk-$(SentryAndroidSdkVersion).pom + + + + + + + + + + + From cabaa599a7343cd4ef00fce723c4404e7929fb7e Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 27 Jan 2026 11:49:14 +1300 Subject: [PATCH 06/20] Fix issues building against local maven repository --- .../DefaultReplayBreadcrumbConverter.cs | 7 ++++--- .../Sentry.Bindings.Android.csproj | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs index 252f059a3c..0739fa7030 100644 --- a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs +++ b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs @@ -4,7 +4,8 @@ // ReSharper disable once CheckNamespace - match generated code namespace namespace Sentry.JavaSdk.Android.Replay; -// This file used to try to "patch" the generated bindings via a partial class. -// It caused build breaks when the generated Java binding shape changed. -// Binding fixes should be done via Transforms/Metadata.xml instead. +// Add IReplayBreadcrumbConverter interface to DefaultReplayBreadcrumbConverter, to work source generator issue +internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter +{ +} #endif diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index a0596b454e..8f02744c53 100644 --- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj +++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj @@ -19,7 +19,7 @@ Note that you may need to check out specific branches/tags to match the version defined in SentryAndroidSdkVersion and the resulting native ndk version. --> - false + true $(HOME)/.m2/repository/ $(LocalSentryMavenRepoDir)io/sentry/sentry/$(SentryAndroidSdkVersion)/sentry-$(SentryAndroidSdkVersion).jar $(LocalSentryMavenRepoDir)io/sentry/sentry-android-core/$(SentryAndroidSdkVersion)/sentry-android-core-$(SentryAndroidSdkVersion).aar @@ -107,11 +107,11 @@ - - - From 4cdf36c3abb449eafebe01fd557894ea7753d8e4 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 27 Jan 2026 13:04:34 +1300 Subject: [PATCH 07/20] Remove unecessary assignment of default converter --- src/Sentry/Platforms/Android/SentrySdk.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 90b69552e5..6b92b601ec 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -144,11 +144,6 @@ private static void InitSentryAndroidSdk(SentryOptions options) (JavaDouble?)options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate; o.SessionReplay.SetMaskAllImages(options.Native.ExperimentalOptions.SessionReplay.MaskAllImages); o.SessionReplay.SetMaskAllText(options.Native.ExperimentalOptions.SessionReplay.MaskAllText); - // Set the BreadcrumbConverter to the DefaultReplayBreadcrumbConverter - if (o.ReplayController is { } replayController) - { - replayController.BreadcrumbConverter = new Sentry.JavaSdk.Android.Replay.DefaultReplayBreadcrumbConverter(o); - } // These options are intentionally set and not exposed for modification o.EnableExternalConfiguration = false; From d05cde3dd0f1e099dba218255f3fc0cb9b155939 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 27 Jan 2026 00:26:19 +0000 Subject: [PATCH 08/20] Format code --- src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs index 0739fa7030..a31c54a483 100644 --- a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs +++ b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs @@ -5,7 +5,7 @@ namespace Sentry.JavaSdk.Android.Replay; // Add IReplayBreadcrumbConverter interface to DefaultReplayBreadcrumbConverter, to work source generator issue -internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter +internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter { } #endif From 0b6c66c0f1897daa347c97ad5754f0e46b0b96b0 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Tue, 27 Jan 2026 22:56:21 +1300 Subject: [PATCH 09/20] Adding sample code temporarily --- samples/Sentry.Samples.Maui/MainPage.xaml.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/samples/Sentry.Samples.Maui/MainPage.xaml.cs b/samples/Sentry.Samples.Maui/MainPage.xaml.cs index b82a13787d..5b7a09dee6 100644 --- a/samples/Sentry.Samples.Maui/MainPage.xaml.cs +++ b/samples/Sentry.Samples.Maui/MainPage.xaml.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Microsoft.Extensions.Logging; namespace Sentry.Samples.Maui; @@ -31,6 +32,19 @@ private void OnCounterClicked(object sender, EventArgs e) { _count++; + var span = SentrySdk.StartSpan("button.click", "ui.action"); + try + { + SentrySdk.ConfigureScope(scope => scope.Transaction ??= span.GetTransaction()); + // Make an HTTP request to demonstrate Sentry's automatic HTTP tracking + using var httpClient = new HttpClient(new SentryHttpMessageHandler()); + httpClient.GetAsync("https://www.example.com/").GetAwaiter().GetResult(); + span.Status = SpanStatus.Ok; + } + finally + { + span.Finish(); + } if (_count == 1) { CounterBtn.Text = $"Clicked {_count} time"; From 96e9e26f2302114f9c1d7e342ceb8f35d5edfeb8 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Jan 2026 15:12:37 +1300 Subject: [PATCH 10/20] Added custom breadcrumb converter to deal with the conversion between .NET and Java replay breadcrumbs --- .../DefaultReplayBreadcrumbConverter.cs | 11 --- .../DotnetReplayBreadcrumbConverter.cs | 77 +++++++++++++++++++ src/Sentry/Platforms/Android/SentrySdk.cs | 4 + 3 files changed, 81 insertions(+), 11 deletions(-) delete mode 100644 src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs create mode 100644 src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs diff --git a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs b/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs deleted file mode 100644 index a31c54a483..0000000000 --- a/src/Sentry.Bindings.Android/DefaultReplayBreadcrumbConverter.cs +++ /dev/null @@ -1,11 +0,0 @@ -#if __ANDROID__ -using Sentry.JavaSdk; - -// ReSharper disable once CheckNamespace - match generated code namespace -namespace Sentry.JavaSdk.Android.Replay; - -// Add IReplayBreadcrumbConverter interface to DefaultReplayBreadcrumbConverter, to work source generator issue -internal partial class DefaultReplayBreadcrumbConverter : IReplayBreadcrumbConverter -{ -} -#endif diff --git a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs new file mode 100644 index 0000000000..85a05f4b62 --- /dev/null +++ b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs @@ -0,0 +1,77 @@ +using Sentry.JavaSdk; +using Sentry.JavaSdk.Android.Replay; +using System.Globalization; + +// ReSharper disable once CheckNamespace - match generated code namespace +namespace Sentry; + +// Add IReplayBreadcrumbConverter interface to DefaultReplayBreadcrumbConverter, to work source generator issue +internal class DotnetReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter, IReplayBreadcrumbConverter +{ + private const string HttpCategory = "http"; + + // Kotlin expects these keys to be Double or Long (ms) + private const string HttpStartTimestampKey = "http.start_timestamp"; + private const string HttpEndTimestampKey = "http.end_timestamp"; + + public DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions options) : base(options) + { + } + + public override global::IO.Sentry.Rrweb.RRWebEvent? Convert(Sentry.JavaSdk.Breadcrumb breadcrumb) + { + if (breadcrumb is null) + { + return null; + } + + // The Java converter expects httpStartTimestamp/httpEndTimestamp to be Double or Long. + // .NET breadcrumb data is always stored as strings. We convert these to numeric here so that the base.Convert() + // method doesn't throw an exception. + try + { + if (breadcrumb.Category == HttpCategory && breadcrumb.Data is { } data) + { + NormalizeTimestampField(data, HttpStartTimestampKey); + NormalizeTimestampField(data, HttpEndTimestampKey); + } + } + catch + { + // Best-effort: never fail conversion because of parsing issues... we may be parsing breadcrumbs that don't + // originate from the .NET SDK. + } + + return base.Convert(breadcrumb); + } + + private static void NormalizeTimestampField(IDictionary data, string key) + { + data.TryGetValue(key, out var value); + if (value is null or Java.Lang.Long or Java.Lang.Double or Java.Lang.Integer or Java.Lang.Float) + { + return; + } + + // Note: `data.Get` returns `Java.Lang.Object`, not a .NET `string`. + var str = (value as Java.Lang.String)?.ToString() ?? value.ToString(); + if (string.IsNullOrWhiteSpace(str)) + { + return; + } + + // Prefer integer milliseconds, but accept floating point too. + // Use invariant culture to avoid commas, etc. + if (long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var asLong)) + { + data[key] = Java.Lang.Long.ValueOf(asLong); + return; + } + + if (double.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out var asDouble)) + { + // Preserve type as Double; Java converter divides by 1000.0. + data[key] = Java.Lang.Double.ValueOf(asDouble); + } + } +} diff --git a/src/Sentry/Platforms/Android/SentrySdk.cs b/src/Sentry/Platforms/Android/SentrySdk.cs index 6b92b601ec..919ec00357 100644 --- a/src/Sentry/Platforms/Android/SentrySdk.cs +++ b/src/Sentry/Platforms/Android/SentrySdk.cs @@ -144,6 +144,10 @@ private static void InitSentryAndroidSdk(SentryOptions options) (JavaDouble?)options.Native.ExperimentalOptions.SessionReplay.SessionSampleRate; o.SessionReplay.SetMaskAllImages(options.Native.ExperimentalOptions.SessionReplay.MaskAllImages); o.SessionReplay.SetMaskAllText(options.Native.ExperimentalOptions.SessionReplay.MaskAllText); + if (o.ReplayController is { } replayController) + { + replayController.BreadcrumbConverter = new DotnetReplayBreadcrumbConverter(o); + } // These options are intentionally set and not exposed for modification o.EnableExternalConfiguration = false; From c3a38897237de22830596169dde0df454c2c77c1 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Jan 2026 15:28:05 +1300 Subject: [PATCH 11/20] Remove unecessary changes refactored to other PRs --- samples/Sentry.Samples.Maui/MainPage.xaml.cs | 13 ----------- src/Sentry.Bindings.Android/README.md | 2 +- .../Transforms/Metadata.xml | 3 +++ .../DotnetReplayBreadcrumbConverter.cs | 22 ++++--------------- 4 files changed, 8 insertions(+), 32 deletions(-) diff --git a/samples/Sentry.Samples.Maui/MainPage.xaml.cs b/samples/Sentry.Samples.Maui/MainPage.xaml.cs index 5b7a09dee6..9ea62da935 100644 --- a/samples/Sentry.Samples.Maui/MainPage.xaml.cs +++ b/samples/Sentry.Samples.Maui/MainPage.xaml.cs @@ -32,19 +32,6 @@ private void OnCounterClicked(object sender, EventArgs e) { _count++; - var span = SentrySdk.StartSpan("button.click", "ui.action"); - try - { - SentrySdk.ConfigureScope(scope => scope.Transaction ??= span.GetTransaction()); - // Make an HTTP request to demonstrate Sentry's automatic HTTP tracking - using var httpClient = new HttpClient(new SentryHttpMessageHandler()); - httpClient.GetAsync("https://www.example.com/").GetAwaiter().GetResult(); - span.Status = SpanStatus.Ok; - } - finally - { - span.Finish(); - } if (_count == 1) { CounterBtn.Text = $"Clicked {_count} time"; diff --git a/src/Sentry.Bindings.Android/README.md b/src/Sentry.Bindings.Android/README.md index 4312b1b37e..3981ca163f 100644 --- a/src/Sentry.Bindings.Android/README.md +++ b/src/Sentry.Bindings.Android/README.md @@ -6,7 +6,7 @@ Instead, reference one of the following: ## SDK Developers and Contributors -For .NET SDK contributors, most of the classes in this package are generated automatically. These can be found in the `src/Sentry.Bindings.Android/obj/Debug/{tfm}/generated/src/` folder after building the project. +For .NET SDK contributors, most of the classes in this package are generated automatically. - Post generation transformations are controlled via various XML files stored in the `/Transforms` directory (see [Java Bindings Metadata documentation](https://learn.microsoft.com/en-gb/previous-versions/xamarin/android/platform/binding-java-library/customizing-bindings/java-bindings-metadata) for details). diff --git a/src/Sentry.Bindings.Android/Transforms/Metadata.xml b/src/Sentry.Bindings.Android/Transforms/Metadata.xml index 1a0c8bb6e9..9290a1a1c5 100644 --- a/src/Sentry.Bindings.Android/Transforms/Metadata.xml +++ b/src/Sentry.Bindings.Android/Transforms/Metadata.xml @@ -163,4 +163,7 @@ + + + diff --git a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs index 85a05f4b62..a04e3add16 100644 --- a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs +++ b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs @@ -1,36 +1,24 @@ using Sentry.JavaSdk; using Sentry.JavaSdk.Android.Replay; -using System.Globalization; -// ReSharper disable once CheckNamespace - match generated code namespace -namespace Sentry; +namespace Sentry.Android; -// Add IReplayBreadcrumbConverter interface to DefaultReplayBreadcrumbConverter, to work source generator issue -internal class DotnetReplayBreadcrumbConverter : DefaultReplayBreadcrumbConverter, IReplayBreadcrumbConverter +internal class DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions options) + : DefaultReplayBreadcrumbConverter(options), IReplayBreadcrumbConverter { private const string HttpCategory = "http"; - // Kotlin expects these keys to be Double or Long (ms) private const string HttpStartTimestampKey = "http.start_timestamp"; private const string HttpEndTimestampKey = "http.end_timestamp"; - public DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions options) : base(options) - { - } - public override global::IO.Sentry.Rrweb.RRWebEvent? Convert(Sentry.JavaSdk.Breadcrumb breadcrumb) { - if (breadcrumb is null) - { - return null; - } - // The Java converter expects httpStartTimestamp/httpEndTimestamp to be Double or Long. // .NET breadcrumb data is always stored as strings. We convert these to numeric here so that the base.Convert() // method doesn't throw an exception. try { - if (breadcrumb.Category == HttpCategory && breadcrumb.Data is { } data) + if (breadcrumb is { Category: HttpCategory, Data: { } data }) { NormalizeTimestampField(data, HttpStartTimestampKey); NormalizeTimestampField(data, HttpEndTimestampKey); @@ -60,8 +48,6 @@ private static void NormalizeTimestampField(IDictionary Date: Wed, 28 Jan 2026 15:31:47 +1300 Subject: [PATCH 12/20] Remove changes refactored to #4873 --- .../Sentry.Bindings.Android.csproj | 58 +------------------ 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj index 8f02744c53..3da14848ad 100644 --- a/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj +++ b/src/Sentry.Bindings.Android/Sentry.Bindings.Android.csproj @@ -3,33 +3,10 @@ $(LatestAndroidTfm);$(PreviousAndroidTfm) 8.29.0 $(BaseIntermediateOutputPath)sdks\$(TargetFramework)\Sentry\Android\$(SentryAndroidSdkVersion)\ - + .NET Bindings for the Sentry Android SDK - - true - $(HOME)/.m2/repository/ - $(LocalSentryMavenRepoDir)io/sentry/sentry/$(SentryAndroidSdkVersion)/sentry-$(SentryAndroidSdkVersion).jar - $(LocalSentryMavenRepoDir)io/sentry/sentry-android-core/$(SentryAndroidSdkVersion)/sentry-android-core-$(SentryAndroidSdkVersion).aar - $(LocalSentryMavenRepoDir)io/sentry/sentry-android-ndk/$(SentryAndroidSdkVersion)/sentry-android-ndk-$(SentryAndroidSdkVersion).aar - $(LocalSentryMavenRepoDir)io/sentry/sentry-android-replay/$(SentryAndroidSdkVersion)/sentry-android-replay-$(SentryAndroidSdkVersion).aar - $([System.String]::Copy($(LocalSentryJarPath)).Replace('.jar', '.pom')) - $([System.String]::Copy($(LocalSentryAndroidCoreAarPath)).Replace('.aar', '.pom')) - $([System.String]::Copy($(LocalSentryAndroidNdkAarPath)).Replace('.aar', '.pom')) - $([System.String]::Copy($(LocalSentryAndroidReplayAarPath)).Replace('.aar', '.pom')) - @@ -96,25 +73,13 @@ - + - - - - - - - - @@ -123,7 +88,7 @@ - + - - - <_LocalAndroidNdkPom>$(LocalSentryMavenRepoDir)io/sentry/sentry-android-ndk/$(SentryAndroidSdkVersion)/sentry-android-ndk-$(SentryAndroidSdkVersion).pom - - - - - - - - - - - From a40a7a0e3f39041b2e13583bb3b1174a8798972c Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Jan 2026 15:40:01 +1300 Subject: [PATCH 13/20] Consolidate constants --- .../Platforms/Android/DotnetReplayBreadcrumbConverter.cs | 7 ++----- src/Sentry/SentryHttpMessageHandler.cs | 6 ++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs index a04e3add16..da9ca72c88 100644 --- a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs +++ b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs @@ -8,9 +8,6 @@ internal class DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions opti { private const string HttpCategory = "http"; - private const string HttpStartTimestampKey = "http.start_timestamp"; - private const string HttpEndTimestampKey = "http.end_timestamp"; - public override global::IO.Sentry.Rrweb.RRWebEvent? Convert(Sentry.JavaSdk.Breadcrumb breadcrumb) { // The Java converter expects httpStartTimestamp/httpEndTimestamp to be Double or Long. @@ -20,8 +17,8 @@ internal class DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions opti { if (breadcrumb is { Category: HttpCategory, Data: { } data }) { - NormalizeTimestampField(data, HttpStartTimestampKey); - NormalizeTimestampField(data, HttpEndTimestampKey); + NormalizeTimestampField(data, SentryHttpMessageHandler.HttpStartTimestampKey); + NormalizeTimestampField(data, SentryHttpMessageHandler.HttpEndTimestampKey); } } catch diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index 863164e9d0..7792bbc2d6 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -15,6 +15,8 @@ public class SentryHttpMessageHandler : SentryMessageHandler private readonly ISentryFailedRequestHandler? _failedRequestHandler; internal const string HttpClientOrigin = "auto.http.client"; + internal const string HttpStartTimestampKey = "http.start_timestamp"; + internal const string HttpEndTimestampKey = "http.end_timestamp"; /// /// Constructs an instance of . @@ -93,8 +95,8 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS { // 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["http.start_timestamp"] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); - breadcrumbData["http.end_timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); + breadcrumbData[HttpStartTimestampKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); + breadcrumbData[HttpEndTimestampKey] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); } _hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData); From 85d47f71ca126c2918e9094090daa9b1cf24a61a Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Wed, 28 Jan 2026 20:00:01 +1300 Subject: [PATCH 14/20] message handler tests --- .../SentryHttpMessageHandlerTests.cs | 73 ++++++++++++++++--- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index 584fcd43d7..37488a778c 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -611,25 +611,80 @@ public void Send_Executed_BreadcrumbCreated() Assert.True(breadcrumbGenerated.Data.ContainsKey(statusKey)); Assert.Equal(expectedBreadcrumbData[statusKey], breadcrumbGenerated.Data[statusKey]); } +#endif [Fact] - public void Send_Executed_FailedRequestsCaptured() + public void HandleResponse_SpanExists_AddsReplayBreadcrumbData() { // Arrange + var scope = new Scope(); var hub = Substitute.For(); - var failedRequestHandler = Substitute.For(); - 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(); + span.StartTimestamp.Returns(DateTimeOffset.UtcNow.AddMilliseconds(-50)); // Act - client.Get(url); + sut.HandleResponse(response, span, method, url); // Assert - failedRequestHandler.Received(1).HandleResponse(Arg.Any()); + var breadcrumb = scope.Breadcrumbs.First(); + breadcrumb.Type.Should().Be("http"); + breadcrumb.Category.Should().Be("http"); + + breadcrumb.Data.Should().NotBeNull(); + 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); + endMs.Should().BeGreaterThan(0); + endMs.Should().BeGreaterOrEqualTo(startMs); + + // Sanity: start should match span start (ms resolution) + startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds()); + + // Ensure response sets status code on span + span.Extra.Should().ContainKey(OtelSemanticConventions.AttributeHttpResponseStatusCode); + span.Extra[OtelSemanticConventions.AttributeHttpResponseStatusCode].Should().Be((int)HttpStatusCode.OK); + } + + [Fact] + public void HandleResponse_NoSpanExists_NoReplayBreadcrumbData() + { + // Arrange + var scope = new Scope(); + var hub = Substitute.For(); + 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); } -#endif } From e51db440a5e882ce80bfa067ad69f537210bbad7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 29 Jan 2026 12:26:16 +1300 Subject: [PATCH 15/20] fixed test --- test/Sentry.Tests/SentryHttpMessageHandlerTests.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index 37488a778c..d7edeff711 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -653,15 +653,9 @@ public void HandleResponse_SpanExists_AddsReplayBreadcrumbData() .Should().BeTrue(); startMs.Should().BeGreaterThan(0); + startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds()); endMs.Should().BeGreaterThan(0); endMs.Should().BeGreaterOrEqualTo(startMs); - - // Sanity: start should match span start (ms resolution) - startMs.Should().Be(span.StartTimestamp.ToUnixTimeMilliseconds()); - - // Ensure response sets status code on span - span.Extra.Should().ContainKey(OtelSemanticConventions.AttributeHttpResponseStatusCode); - span.Extra[OtelSemanticConventions.AttributeHttpResponseStatusCode].Should().Be((int)HttpStatusCode.OK); } [Fact] From 2c847fe295581168b7c46e22ec1e9119d32fdbf7 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 29 Jan 2026 13:15:37 +1300 Subject: [PATCH 16/20] Added DotnetReplayBreadcrumbConverterTests --- .../DotnetReplayBreadcrumbConverterTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs diff --git a/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs b/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs new file mode 100644 index 0000000000..fdd1f9c4df --- /dev/null +++ b/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs @@ -0,0 +1,37 @@ +#if ANDROID +using Sentry.Android; + +namespace Sentry.Tests.Platforms.Android; + +public class DotnetReplayBreadcrumbConverterTests +{ + [Fact] + public void Convert_HttpBreadcrumbWithStringTimestamps_ConvertsToNumeric() + { + // Arrange + var options = new Sentry.JavaSdk.SentryOptions(); + var converter = new DotnetReplayBreadcrumbConverter(options); + var breadcrumb = new Sentry.JavaSdk.Breadcrumb + { + Category = "http", + Data = + { + { "url", "https://example.com" }, + { SentryHttpMessageHandler.HttpStartTimestampKey, "1625079600000" }, + { SentryHttpMessageHandler.HttpEndTimestampKey, "1625079660000" } + } + }; + + // Act + var rrwebEvent = converter.Convert(breadcrumb); + + // Assert + rrwebEvent.Should().BeOfType(); + var rrWebSpanEvent = rrwebEvent as IO.Sentry.Rrweb.RRWebSpanEvent; + Assert.NotNull(rrWebSpanEvent); + // Note the converter divides by 1000 to get ms + rrWebSpanEvent.StartTimestamp.Should().Be(1625079600L); + rrWebSpanEvent.EndTimestamp.Should().Be(1625079660L); + } +} +#endif From 6da450a9daa22aacc95ac7b060142fdfc7d8e370 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 29 Jan 2026 13:23:15 +1300 Subject: [PATCH 17/20] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26bbf20371..da7709d58e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - 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)) ### Dependencies From 4abdbec48166b534f9c2ddb8abdcfbabf7401bdb Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 5 Feb 2026 15:44:57 +1300 Subject: [PATCH 18/20] Remove unintended change to samples --- samples/Sentry.Samples.Maui/MainPage.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/Sentry.Samples.Maui/MainPage.xaml.cs b/samples/Sentry.Samples.Maui/MainPage.xaml.cs index 9ea62da935..b82a13787d 100644 --- a/samples/Sentry.Samples.Maui/MainPage.xaml.cs +++ b/samples/Sentry.Samples.Maui/MainPage.xaml.cs @@ -1,4 +1,3 @@ -using System.Globalization; using Microsoft.Extensions.Logging; namespace Sentry.Samples.Maui; From 0f2f11417083be2b9d5096b7cdf7355dc92373b2 Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 6 Feb 2026 13:44:27 +1300 Subject: [PATCH 19/20] Fix sample app so it can be run in Rider --- samples/Sentry.Samples.Maui/Properties/launchSettings.json | 3 +++ samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/samples/Sentry.Samples.Maui/Properties/launchSettings.json b/samples/Sentry.Samples.Maui/Properties/launchSettings.json index bc20df0c32..f7ce884b65 100644 --- a/samples/Sentry.Samples.Maui/Properties/launchSettings.json +++ b/samples/Sentry.Samples.Maui/Properties/launchSettings.json @@ -1,5 +1,8 @@ { "profiles": { + "Android": { + "commandName": "Project" + }, "Windows Machine": { "commandName": "MsixPackage", "nativeDebugging": false diff --git a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj index fe4b910261..54a7a914dc 100644 --- a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj +++ b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj @@ -69,12 +69,10 @@ $([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture) $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) - android-arm64 ios-arm64 iossimulator-arm64 maccatalyst-arm64 - android-x64 iossimulator-x64 maccatalyst-x64 From 550a5c425458f9f47ee72fc1ca9bd9f53a6da01f Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Fri, 6 Feb 2026 14:50:02 +1300 Subject: [PATCH 20/20] Review feedback --- .../DotnetReplayBreadcrumbConverter.cs | 12 +++++----- src/Sentry/SentryHttpMessageHandler.cs | 2 ++ .../DotnetReplayBreadcrumbConverterTests.cs | 3 ++- .../SentryHttpMessageHandlerTests.cs | 23 +++++++++++++++++++ 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs index da9ca72c88..27cab84a2f 100644 --- a/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs +++ b/src/Sentry/Platforms/Android/DotnetReplayBreadcrumbConverter.cs @@ -10,9 +10,11 @@ internal class DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions opti public override global::IO.Sentry.Rrweb.RRWebEvent? Convert(Sentry.JavaSdk.Breadcrumb breadcrumb) { - // The Java converter expects httpStartTimestamp/httpEndTimestamp to be Double or Long. - // .NET breadcrumb data is always stored as strings. We convert these to numeric here so that the base.Convert() - // method doesn't throw an exception. + // The Java SDK automatically converts breadcrumbs for outgoing http requests into performance spans + // that show in the Network tab of session replays... however, it expects certain data to be stored in a + // specific format in the breadcrumb.data. It needs values for httpStartTimestamp and httpEndTimestamp + // stored as Double or Long representations of timestamps (milliseconds since epoch). + // .NET breadcrumb data is always stored as strings, so we have to convert these to numeric values here. try { if (breadcrumb is { Category: HttpCategory, Data: { } data }) @@ -32,13 +34,11 @@ internal class DotnetReplayBreadcrumbConverter(Sentry.JavaSdk.SentryOptions opti private static void NormalizeTimestampField(IDictionary data, string key) { - data.TryGetValue(key, out var value); - if (value is null or Java.Lang.Long or Java.Lang.Double or Java.Lang.Integer or Java.Lang.Float) + if (!data.TryGetValue(key, out var value) || value is Java.Lang.Number) { return; } - // Note: `data.Get` returns `Java.Lang.Object`, not a .NET `string`. var str = (value as Java.Lang.String)?.ToString() ?? value.ToString(); if (string.IsNullOrWhiteSpace(str)) { diff --git a/src/Sentry/SentryHttpMessageHandler.cs b/src/Sentry/SentryHttpMessageHandler.cs index 7792bbc2d6..6f8489690d 100644 --- a/src/Sentry/SentryHttpMessageHandler.cs +++ b/src/Sentry/SentryHttpMessageHandler.cs @@ -91,6 +91,7 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS {"method", method}, {"status_code", ((int) response.StatusCode).ToString()} }; +#if ANDROID if (span is not null) { // Ensure the breadcrumb can be converted to RRWeb so that it shows up in the network tab in Session Replay. @@ -98,6 +99,7 @@ protected internal override void HandleResponse(HttpResponseMessage response, IS breadcrumbData[HttpStartTimestampKey] = span.StartTimestamp.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); breadcrumbData[HttpEndTimestampKey] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString("F0", CultureInfo.InvariantCulture); } +#endif _hub.AddBreadcrumb(string.Empty, "http", "http", breadcrumbData); // Create events for failed requests diff --git a/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs b/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs index fdd1f9c4df..d92c083dd5 100644 --- a/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs +++ b/test/Sentry.Tests/Platforms/Android/DotnetReplayBreadcrumbConverterTests.cs @@ -29,7 +29,8 @@ public void Convert_HttpBreadcrumbWithStringTimestamps_ConvertsToNumeric() rrwebEvent.Should().BeOfType(); var rrWebSpanEvent = rrwebEvent as IO.Sentry.Rrweb.RRWebSpanEvent; Assert.NotNull(rrWebSpanEvent); - // Note the converter divides by 1000 to get ms + // Note the converter divides by 1000 to get ms. + // See https://github.com/getsentry/sentry-java/blob/94bff8dc0a952ad8c1b6815a9eda5005e41b92c7/sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt#L215-L228 rrWebSpanEvent.StartTimestamp.Should().Be(1625079600L); rrWebSpanEvent.EndTimestamp.Should().Be(1625079660L); } diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index d7edeff711..f21d2a2237 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -611,8 +611,30 @@ 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(); + var failedRequestHandler = Substitute.For(); + 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()); + } + #endif +#if ANDROID [Fact] public void HandleResponse_SpanExists_AddsReplayBreadcrumbData() { @@ -681,4 +703,5 @@ public void HandleResponse_NoSpanExists_NoReplayBreadcrumbData() breadcrumb.Data!.Should().NotContainKey(SentryHttpMessageHandler.HttpStartTimestampKey); breadcrumb.Data.Should().NotContainKey(SentryHttpMessageHandler.HttpEndTimestampKey); } +#endif }