diff --git a/modules/sentry-native b/modules/sentry-native index ccef7125b3..77dbf6a600 160000 --- a/modules/sentry-native +++ b/modules/sentry-native @@ -1 +1 @@ -Subproject commit ccef7125b3783210f44ee6b100df7278e6ba3eff +Subproject commit 77dbf6a6006f0be24af30d056bdcac98a7bf5877 diff --git a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj index 28c0f0d502..bf92b361fa 100644 --- a/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj +++ b/samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj @@ -6,9 +6,9 @@ On Mac, we'll also build for iOS and MacCatalyst. On Windows, we'll also build for Windows 10. --> - $(TargetFrameworks);net9.0-android35.0 - $(TargetFrameworks);net9.0-windows10.0.19041.0;net9.0-ios18.0;net9.0-maccatalyst18.0 - $(TargetFrameworks);net9.0-ios18.0;net9.0-maccatalyst18.0 + $(TargetFrameworks);net9.0-android + $(TargetFrameworks);net9.0-windows;net9.0-ios;net9.0-maccatalyst + $(TargetFrameworks);net9.0-ios;net9.0-maccatalyst Exe Sentry.Samples.Maui true diff --git a/src/Sentry.Maui/Internal/IMauiPageEventHandler.cs b/src/Sentry.Maui/Internal/IMauiPageEventHandler.cs new file mode 100644 index 0000000000..a15a457d1a --- /dev/null +++ b/src/Sentry.Maui/Internal/IMauiPageEventHandler.cs @@ -0,0 +1,31 @@ +namespace Sentry.Maui.Internal; + +/// +/// Allows you to receive MAUI page level events without hooking (this list is NOT exhaustive at this time) +/// +public interface IMauiPageEventHandler +{ + /// + /// Page.OnAppearing + /// + /// + public void OnAppearing(Page page); + + /// + /// Page.OnDisappearing + /// + /// + public void OnDisappearing(Page page); + + /// + /// Page.OnNavigatedTo + /// + /// + public void OnNavigatedTo(Page page); + + /// + /// Page.OnNavigatedFrom + /// + /// + public void OnNavigatedFrom(Page page); +} diff --git a/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs b/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs index f9b1dcbcb6..e9cdda5ca1 100644 --- a/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs @@ -1,7 +1,7 @@ namespace Sentry.Maui.Internal; /// -public class MauiButtonEventsBinder : IMauiElementEventBinder +public class MauiButtonEventsBinder(IHub hub) : IMauiElementEventBinder { private Action? addBreadcrumbCallback; @@ -31,7 +31,14 @@ public void UnBind(VisualElement element) private void OnButtonOnClicked(object? sender, EventArgs _) - => addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked))); + { + hub.ConfigureScope(scope => + { + // scope.Transaction.SetMeasurement(); + }); + addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Clicked))); + } + private void OnButtonOnPressed(object? sender, EventArgs _) => addBreadcrumbCallback?.Invoke(new(sender, nameof(Button.Pressed))); diff --git a/src/Sentry.Maui/Internal/MauiEventsBinder.cs b/src/Sentry.Maui/Internal/MauiEventsBinder.cs index 08f6d91dd7..cd97bccb3d 100644 --- a/src/Sentry.Maui/Internal/MauiEventsBinder.cs +++ b/src/Sentry.Maui/Internal/MauiEventsBinder.cs @@ -1,4 +1,8 @@ +using System; using Microsoft.Extensions.Options; +using Microsoft.Maui.Controls; +using Sentry.Internal; +using Sentry.Protocol; namespace Sentry.Maui.Internal; @@ -12,6 +16,7 @@ internal class MauiEventsBinder : IMauiEventsBinder private readonly IHub _hub; private readonly SentryMauiOptions _options; private readonly IEnumerable _elementEventBinders; + private readonly IEnumerable _pageEventHandlers; // https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types // https://github.com/getsentry/sentry/blob/master/static/app/types/breadcrumbs.tsx @@ -23,11 +28,13 @@ internal class MauiEventsBinder : IMauiEventsBinder internal const string RenderingCategory = "ui.rendering"; internal const string UserActionCategory = "ui.useraction"; - public MauiEventsBinder(IHub hub, IOptions options, IEnumerable elementEventBinders) + + public MauiEventsBinder(IHub hub, IOptions options, IEnumerable elementEventBinders, IEnumerable pageEventHandlers) { _hub = hub; _options = options.Value; _elementEventBinders = elementEventBinders; + _pageEventHandlers = pageEventHandlers; } public void HandleApplicationEvents(Application application, bool bind = true) @@ -313,10 +320,18 @@ internal void HandlePageEvents(Page page, bool bind = true) // Application Events - private void OnApplicationOnPageAppearing(object? sender, Page page) => + private void OnApplicationOnPageAppearing(object? sender, Page page) + { _hub.AddBreadcrumbForEvent(_options, sender, nameof(Application.PageAppearing), NavigationType, NavigationCategory, data => data.AddElementInfo(_options, page, nameof(Page))); - private void OnApplicationOnPageDisappearing(object? sender, Page page) => + RunPageEventHandlers(handler => handler.OnAppearing(page)); + } + + private void OnApplicationOnPageDisappearing(object? sender, Page page) + { _hub.AddBreadcrumbForEvent(_options, sender, nameof(Application.PageDisappearing), NavigationType, NavigationCategory, data => data.AddElementInfo(_options, page, nameof(Page))); + RunPageEventHandlers(handler => handler.OnDisappearing(page)); + } + private void OnApplicationOnModalPushed(object? sender, ModalPushedEventArgs e) => _hub.AddBreadcrumbForEvent(_options, sender, nameof(Application.ModalPushed), NavigationType, NavigationCategory, data => data.AddElementInfo(_options, e.Modal, nameof(e.Modal))); private void OnApplicationOnModalPopped(object? sender, ModalPoppedEventArgs e) => @@ -440,4 +455,10 @@ private void OnPageOnNavigatedTo(object? sender, NavigatedToEventArgs e) => private void OnPageOnLayoutChanged(object? sender, EventArgs _) => _hub.AddBreadcrumbForEvent(_options, sender, nameof(Page.LayoutChanged), SystemType, RenderingCategory); + + private void RunPageEventHandlers(Action action) + { + foreach (var handler in _pageEventHandlers) + action(handler); // TODO: try/catch in case of user code? + } } diff --git a/src/Sentry.Maui/Internal/TtdMauiPageEventHandler.cs b/src/Sentry.Maui/Internal/TtdMauiPageEventHandler.cs new file mode 100644 index 0000000000..6b72d1eda3 --- /dev/null +++ b/src/Sentry.Maui/Internal/TtdMauiPageEventHandler.cs @@ -0,0 +1,79 @@ +using Sentry.Internal; +using Sentry.Internal.Extensions; + +namespace Sentry.Maui.Internal; + +/// +/// Time-to-(initial/full)-display page event handler +/// https://docs.sentry.io/product/insights/mobile/mobile-vitals/ +/// +internal class TtdMauiPageEventHandler(IHub hub) : IMauiPageEventHandler +{ + internal static long? StartupTimestamp { get; set; } + + // [MobileVital.AppStartCold]: 'duration', + // [MobileVital.AppStartWarm]: 'duration', + // [MobileVital.FramesTotal]: 'integer', + // [MobileVital.FramesSlow]: 'integer', + // [MobileVital.FramesFrozen]: 'integer', + // [MobileVital.FramesSlowRate]: 'percentage', + // [MobileVital.FramesFrozenRate]: 'percentage', + // [MobileVital.StallCount]: 'integer', + // [MobileVital.StallTotalTime]: 'duration', + // [MobileVital.StallLongestTime]: 'duration', + // [MobileVital.StallPercentage]: 'percentage', + + internal const string LoadCategory = "ui.load"; + internal const string InitialDisplayType = "initial_display"; + internal const string FullDisplayType = "full_display"; + private bool _ttidRan = false; // this should require thread safety + private ISpan? _timeToInitialDisplaySpan; + private ITransactionTracer? _transaction; + + /// + public async void OnAppearing(Page page) + { + if (_ttidRan && StartupTimestamp != null) + return; + + // if (Interlocked.Exchange(ref _ttidRan, true)) + // return; + + //DispatchTime.Now.Nanoseconds + _ttidRan = true; + var startupTimestamp = ProcessInfo.Instance!.StartupTimestamp; + var screenName = page.GetType().FullName ?? "root /"; + _transaction = hub.StartTransaction( + LoadCategory, + "start" + ); + var elapsedTime = Stopwatch.GetElapsedTime(startupTimestamp); + + _timeToInitialDisplaySpan = _transaction.StartChild(InitialDisplayType, $"{screenName} initial display", ProcessInfo.Instance!.StartupTime); + _timeToInitialDisplaySpan.SetMeasurement("test", elapsedTime.TotalMilliseconds, MeasurementUnit.Parse("ms")); + _timeToInitialDisplaySpan.Finish(); + + // we allow 200ms for the user to start any async tasks with spans + await Task.Delay(200).ConfigureAwait(false); + + try + { + using var cts = new CancellationTokenSource(); + cts.CancelAfterSafe(TimeSpan.FromSeconds(30)); + + // TODO: grab last span and add ttfd measurement + // we're assuming that the user starts any spans around data calls, we wait for those before marking the transaction as finished + await _transaction.FinishWithLastSpanAsync(cts.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + // TODO: what to do? + Console.WriteLine(ex); + } + } + + public void OnDisappearing(Page page) { } + public void OnNavigatedTo(Page page) { } + public void OnNavigatedFrom(Page page) { } +} + diff --git a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs index b10eab38d7..710c312b6a 100644 --- a/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs +++ b/src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs @@ -2,8 +2,10 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Maui.LifecycleEvents; +using Sentry; using Sentry.Extensibility; using Sentry.Extensions.Logging.Extensions.DependencyInjection; +using Sentry.Internal; using Sentry.Maui; using Sentry.Maui.Internal; @@ -42,6 +44,8 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder, string dsn) public static MauiAppBuilder UseSentry(this MauiAppBuilder builder, Action? configureOptions) { + // we set this as early as possible and during the DI phase + TtdMauiPageEventHandler.StartupTimestamp = Stopwatch.GetTimestamp(); var services = builder.Services; if (configureOptions != null) @@ -59,6 +63,7 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder, services.AddSingleton(); services.TryAddSingleton(); + services.AddSingleton(); services.AddSentry(); builder.RegisterMauiEventsBinder(); diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index 8229d57780..c2adbe8065 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -48,7 +48,7 @@ public interface ISentryClient /// /// /// Note: this method is NOT meant to be called from user code! - /// Instead, call on the transaction. + /// Instead, call on the transaction. /// /// The transaction. [EditorBrowsable(EditorBrowsableState.Never)] @@ -59,7 +59,7 @@ public interface ISentryClient /// /// /// Note: this method is NOT meant to be called from user code! - /// Instead, call on the transaction. + /// Instead, call on the transaction. /// /// The transaction. /// The scope to be applied to the transaction diff --git a/src/Sentry/ISpan.cs b/src/Sentry/ISpan.cs index 0259ac02c0..05ebb2475d 100644 --- a/src/Sentry/ISpan.cs +++ b/src/Sentry/ISpan.cs @@ -7,6 +7,11 @@ namespace Sentry; /// public interface ISpan : ISpanData { + /// + /// When the status of the span changes, this event will fire + /// + public event EventHandler? StatusChanged; + /// /// Span description. /// @@ -28,27 +33,27 @@ public interface ISpan : ISpanData /// /// Starts a child span. /// - public ISpan StartChild(string operation); + public ISpan StartChild(string operation, DateTimeOffset? startTime = null); /// /// Finishes the span. - /// - public void Finish(); + /// ` + public void Finish(DateTimeOffset? timestamp = null); /// /// Finishes the span with the specified status. /// - public void Finish(SpanStatus status); + public void Finish(SpanStatus status, DateTimeOffset? timestamp = null); /// /// Finishes the span with the specified exception and status. /// - public void Finish(Exception exception, SpanStatus status); + public void Finish(Exception exception, SpanStatus status, DateTimeOffset? timestamp = null); /// /// Finishes the span with the specified exception and automatically inferred status. /// - public void Finish(Exception exception); + public void Finish(Exception exception, DateTimeOffset? timestamp = null); } /// @@ -60,18 +65,18 @@ public static class SpanExtensions /// /// Starts a child span. /// - public static ISpan StartChild(this ISpan span, string operation, string? description) + public static ISpan StartChild(this ISpan span, string operation, string? description, DateTimeOffset? startTime = null) { - var child = span.StartChild(operation); + var child = span.StartChild(operation, startTime); child.Description = description; return child; } - internal static ISpan StartChild(this ISpan span, SpanContext context) + internal static ISpan StartChild(this ISpan span, SpanContext context, DateTimeOffset? startTime = null) { var transaction = span.GetTransaction() as TransactionTracer; - if (transaction?.StartChild(context.SpanId, span.SpanId, context.Operation, context.Instrumenter) + if (transaction?.StartChild(context.SpanId, span.SpanId, context.Operation, context.Instrumenter, startTime) is not SpanTracer childSpan) { return NoOpSpan.Instance; diff --git a/src/Sentry/Internal/NoOpSpan.cs b/src/Sentry/Internal/NoOpSpan.cs index f6e49e5a2c..f7d5fada65 100644 --- a/src/Sentry/Internal/NoOpSpan.cs +++ b/src/Sentry/Internal/NoOpSpan.cs @@ -30,6 +30,12 @@ public string Operation set { } } + public event EventHandler? StatusChanged + { + add { } + remove { } + } + public string? Description { get => default; @@ -42,21 +48,21 @@ public SpanStatus? Status set { } } - public ISpan StartChild(string operation) => this; + public ISpan StartChild(string operation, DateTimeOffset? startTime) => this; - public void Finish() + public void Finish(DateTimeOffset? timestamp = null) { } - public void Finish(SpanStatus status) + public void Finish(SpanStatus status, DateTimeOffset? timestamp = null) { } - public void Finish(Exception exception, SpanStatus status) + public void Finish(Exception exception, SpanStatus status, DateTimeOffset? timestamp = null) { } - public void Finish(Exception exception) + public void Finish(Exception exception, DateTimeOffset? timestamp = null) { } diff --git a/src/Sentry/Internal/ProcessInfo.cs b/src/Sentry/Internal/ProcessInfo.cs index 414c93e394..5f132d2865 100644 --- a/src/Sentry/Internal/ProcessInfo.cs +++ b/src/Sentry/Internal/ProcessInfo.cs @@ -6,6 +6,12 @@ internal class ProcessInfo { internal static ProcessInfo? Instance; + /// + /// The timespan.GetTimestamp() value at init + /// More precise for determining TTID + /// + internal long StartupTimestamp { get; private set; } = 0L; + /// /// When the code was initialized. /// @@ -58,11 +64,10 @@ internal ProcessInfo( // Fast var now = DateTimeOffset.UtcNow; StartupTime = now; - long? timestamp = 0; try { - timestamp = Stopwatch.GetTimestamp(); - BootTime = now.AddTicks(-timestamp.Value + StartupTimestamp = Stopwatch.GetTimestamp(); + BootTime = now.AddTicks(-StartupTimestamp / (Stopwatch.Frequency / TimeSpan.TicksPerSecond)); } @@ -79,7 +84,7 @@ internal ProcessInfo( options.LogError(e, "Failed to find BootTime: Now {0}, GetTimestamp {1}, Frequency {2}, TicksPerSecond: {3}", now, - timestamp, + StartupTimestamp, Stopwatch.Frequency, TimeSpan.TicksPerSecond); } diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 401a0fa6f0..65aadfdd89 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -524,7 +524,7 @@ public static void CaptureUserFeedback(SentryId eventId, string email, string co /// /// /// Note: this method is NOT meant to be called from user code! - /// Instead, call on the transaction. + /// Instead, call on the transaction. /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] @@ -536,7 +536,7 @@ public static void CaptureTransaction(SentryTransaction transaction) /// /// /// Note: this method is NOT meant to be called from user code! - /// Instead, call on the transaction. + /// Instead, call on the transaction. /// [DebuggerStepThrough] [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/src/Sentry/SpanTracer.cs b/src/Sentry/SpanTracer.cs index ed51d48eaa..0bf06411c0 100644 --- a/src/Sentry/SpanTracer.cs +++ b/src/Sentry/SpanTracer.cs @@ -52,11 +52,26 @@ public void SetMeasurement(string name, Measurement measurement) => /// public string Operation { get; set; } + /// + public event EventHandler? StatusChanged; + /// public string? Description { get; set; } + private SpanStatus? _status; /// - public SpanStatus? Status { get; set; } + public SpanStatus? Status + { + get => _status; + set + { + if (_status == value) + return; + + _status = value; + StatusChanged?.Invoke(this, _status); + } + } /// /// Used by the Sentry.OpenTelemetry.SentrySpanProcessor to mark a span as a Sentry request. Ideally we wouldn't @@ -107,7 +122,8 @@ public SpanTracer( TransactionTracer transaction, SpanId? parentSpanId, SentryId traceId, - string operation) + string operation, + DateTimeOffset? startTimestamp = null) { _hub = hub; Transaction = transaction; @@ -115,7 +131,7 @@ public SpanTracer( ParentSpanId = parentSpanId; TraceId = traceId; Operation = operation; - StartTimestamp = _stopwatch.StartDateTimeOffset; + StartTimestamp = startTimestamp ?? _stopwatch.StartDateTimeOffset; } internal SpanTracer( @@ -125,7 +141,8 @@ internal SpanTracer( SpanId? parentSpanId, SentryId traceId, string operation, - Instrumenter instrumenter = Instrumenter.Sentry) + Instrumenter instrumenter = Instrumenter.Sentry, + DateTimeOffset? startTimestamp = null) { _hub = hub; _instrumenter = instrumenter; @@ -134,11 +151,11 @@ internal SpanTracer( ParentSpanId = parentSpanId; TraceId = traceId; Operation = operation; - StartTimestamp = _stopwatch.StartDateTimeOffset; + StartTimestamp = startTimestamp ?? _stopwatch.StartDateTimeOffset; } /// - public ISpan StartChild(string operation) => Transaction.StartChild(null, parentSpanId: SpanId, operation: operation); + public ISpan StartChild(string operation, DateTimeOffset? timestamp = null) => Transaction.StartChild(null, parentSpanId: SpanId, operation: operation, timestamp: timestamp); /// /// Used to mark a span as unfinished when it was previously marked as finished. This allows us to reuse spans for @@ -151,28 +168,31 @@ internal void Unfinish() } /// - public void Finish() + public void Finish(DateTimeOffset? timestamp = null) { Status ??= SpanStatus.Ok; + if (timestamp is not null) + EndTimestamp = timestamp; + EndTimestamp ??= _stopwatch.CurrentDateTimeOffset; } /// - public void Finish(SpanStatus status) + public void Finish(SpanStatus status, DateTimeOffset? timestamp = null) { Status = status; - Finish(); + Finish(timestamp); } /// - public void Finish(Exception exception, SpanStatus status) + public void Finish(Exception exception, SpanStatus status, DateTimeOffset? timestamp = null) { _hub.BindException(exception, this); - Finish(status); + Finish(status, timestamp); } /// - public void Finish(Exception exception) => Finish(exception, SpanStatusConverter.FromException(exception)); + public void Finish(Exception exception, DateTimeOffset? timestamp = null) => Finish(exception, SpanStatusConverter.FromException(exception), timestamp); /// public SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled); diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index c4da3d5933..802f406071 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -74,6 +74,9 @@ public string Operation set => Contexts.Trace.Operation = value; } + /// + public event EventHandler? StatusChanged; + /// public string? Description { get; set; } @@ -81,7 +84,14 @@ public string Operation public SpanStatus? Status { get => Contexts.Trace.Status; - set => Contexts.Trace.Status = value; + set + { + if (Contexts.Trace.Status == value) + return; + + Contexts.Trace.Status = value; + StatusChanged?.Invoke(this, value); + } } /// @@ -293,10 +303,10 @@ internal TransactionTracer(IHub hub, ITransactionContext context, TimeSpan? idle public void SetMeasurement(string name, Measurement measurement) => _measurements[name] = measurement; /// - public ISpan StartChild(string operation) => StartChild(spanId: null, parentSpanId: SpanId, operation); + public ISpan StartChild(string operation, DateTimeOffset? timestamp = null) => StartChild(spanId: null, parentSpanId: SpanId, operation, timestamp: timestamp); internal ISpan StartChild(SpanId? spanId, SpanId parentSpanId, string operation, - Instrumenter instrumenter = Instrumenter.Sentry) + Instrumenter instrumenter = Instrumenter.Sentry, DateTimeOffset? timestamp = null) { var span = new SpanTracer(_hub, this, SpanId.Create(), parentSpanId, TraceId, operation, instrumenter: instrumenter); if (spanId is { } id) @@ -368,7 +378,7 @@ public void Clear() public ISpan? GetLastActiveSpan() => _activeSpanTracker.PeekActive(); /// - public void Finish() + public void Finish(DateTimeOffset? timestamp = null) { _options?.LogDebug("Attempting to finish Transaction {0}.", SpanId); if (Interlocked.Exchange(ref _cancelIdleTimeout, 0) == 1) @@ -389,6 +399,9 @@ public void Finish() TransactionProfiler?.Finish(); Status ??= SpanStatus.Ok; + if (timestamp != null) + EndTimestamp = timestamp.Value; + EndTimestamp ??= _stopwatch.CurrentDateTimeOffset; _options?.LogDebug("Finished Transaction {0}.", SpanId); @@ -403,22 +416,22 @@ public void Finish() } /// - public void Finish(SpanStatus status) + public void Finish(SpanStatus status, DateTimeOffset? timestamp = null) { Status = status; - Finish(); + Finish(timestamp); } /// - public void Finish(Exception exception, SpanStatus status) + public void Finish(Exception exception, SpanStatus status, DateTimeOffset? timestamp = null) { _hub.BindException(exception, this); - Finish(status); + Finish(status, timestamp); } /// - public void Finish(Exception exception) => - Finish(exception, SpanStatusConverter.FromException(exception)); + public void Finish(Exception exception, DateTimeOffset? timestamp = null) => + Finish(exception, SpanStatusConverter.FromException(exception), timestamp); /// public SentryTraceHeader GetTraceHeader() => new(TraceId, SpanId, IsSampled); diff --git a/src/Sentry/TransactionTracerExtensions.cs b/src/Sentry/TransactionTracerExtensions.cs new file mode 100644 index 0000000000..961c526608 --- /dev/null +++ b/src/Sentry/TransactionTracerExtensions.cs @@ -0,0 +1,94 @@ +namespace Sentry; + + +/// +/// Extensions for ITransactionTracer +/// +public static class TransactionTracerExtensions +{ + /// + /// Waits for the last span to finish + /// + /// + /// + /// + public static async ValueTask FinishWithLastSpanAsync(this ITransactionTracer transaction, CancellationToken cancellationToken = default) + { + if (transaction.IsAllSpansFinished()) + { + var span = transaction.GetLastFinishedSpan(); + if (span != null) + transaction.Finish(span.EndTimestamp); + } + else + { + var span = await transaction.GetLastSpanWhenFinishedAsync(cancellationToken).ConfigureAwait(false); + if (span != null) + transaction.Finish(span.EndTimestamp); + } + } + + /// + /// Checks if all spans are finished within a trnasaction + /// + /// + /// + public static bool IsAllSpansFinished(this ITransactionTracer transaction) + => transaction.Spans.All(x => x.IsFinished); + + + /// + /// Sorts all spans within a transaction by on end timestamp and returns the last span + /// + /// + /// + public static ISpan? GetLastFinishedSpan(this ITransactionTracer transaction) + => transaction.Spans + .ToList() + .Where(x => x.IsFinished) + .OrderByDescending(x => x.EndTimestamp) + .LastOrDefault(x => x.IsFinished); + + /// + /// Gets the last span (if one), when all spans mark themselves as IsFinished: true + /// + /// + /// + /// + public static async Task GetLastSpanWhenFinishedAsync(this ITransactionTracer transaction, CancellationToken cancellationToken = default) + { + // what if no spans + if (transaction.IsAllSpansFinished()) + return transaction.GetLastFinishedSpan(); + + var tcs = new TaskCompletionSource(); + var handler = new EventHandler((_, _) => + { + if (transaction.IsAllSpansFinished()) + { + var lastSpan = transaction.GetLastFinishedSpan(); + tcs.SetResult(lastSpan); + } + }); + + try + { + foreach (var span in transaction.Spans) + { + if (!span.IsFinished) + { + span.StatusChanged += handler; + } + } + + return await tcs.Task.ConfigureAwait(false); + } + finally + { + foreach (var span in transaction.Spans) + { + span.StatusChanged -= handler; + } + } + } +} diff --git a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs index 6d2c7ce1ef..265211cfca 100644 --- a/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs +++ b/test/Sentry.Maui.Tests/MauiEventsBinderTests.cs @@ -23,9 +23,10 @@ public Fixture() hub, options, [ - new MauiButtonEventsBinder(), + new MauiButtonEventsBinder(hub), new MauiImageButtonEventsBinder() - ] + ], + [] ); } }