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()
- ]
+ ],
+ []
);
}
}