Skip to content

[Mobile Vitals] Time-to-initial-display & initial concept for time-to-full-display #4088

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion modules/sentry-native
Submodule sentry-native updated 70 files
+1 −0 .github/ISSUE_TEMPLATE/bug_report.md
+44 −27 .github/workflows/ci.yml
+15 −0 CHANGELOG.md
+32 −2 CMakeLists.txt
+4 −2 README.md
+1 −1 external/crashpad
+16 −7 include/sentry.h
+2 −2 ndk/README.md
+1 −1 ndk/gradle.properties
+27 −18 src/CMakeLists.txt
+9 −0 src/backends/sentry_backend_breakpad.cpp
+17 −0 src/backends/sentry_backend_crashpad.cpp
+4 −4 src/backends/sentry_backend_inproc.c
+6 −0 src/modulefinder/sentry_modulefinder_linux.c
+2 −2 src/modulefinder/sentry_modulefinder_windows.c
+10 −0 src/path/sentry_path_unix.c
+1 −1 src/sentry_backend.h
+27 −11 src/sentry_core.c
+3 −2 src/sentry_core.h
+1 −1 src/sentry_envelope.h
+29 −13 src/sentry_json.c
+1 −1 src/sentry_json.h
+3 −3 src/sentry_options.c
+3 −4 src/sentry_options.h
+12 −7 src/sentry_os.c
+15 −4 src/sentry_scope.c
+2 −2 src/sentry_session.c
+5 −5 src/sentry_string.c
+18 −13 src/sentry_sync.c
+31 −5 src/sentry_sync.h
+16 −16 src/sentry_tracing.c
+6 −6 src/sentry_tracing.h
+2 −2 src/sentry_transport.c
+3 −4 src/sentry_transport.h
+21 −14 src/sentry_utils.c
+8 −1 src/sentry_utils.h
+9 −9 src/sentry_uuid.c
+18 −5 src/sentry_value.c
+1 −11 src/sentry_value.h
+5 −3 src/transports/sentry_transport_winhttp.c
+13 −4 src/unwinder/sentry_unwinder_dbghelp.c
+1 −1 tests/__init__.py
+2 −2 tests/assertions.py
+39 −13 tests/cmake.py
+36 −1 tests/test_integration_crashpad.py
+1 −1 tests/test_integration_http.py
+7 −1 tests/unit/main.c
+12 −1 tests/unit/sentry_testsupport.h
+8 −13 tests/unit/test_attachments.c
+42 −36 tests/unit/test_basic.c
+6 −2 tests/unit/test_concurrency.c
+3 −7 tests/unit/test_consent.c
+4 −5 tests/unit/test_envelopes.c
+1 −1 tests/unit/test_failures.c
+2 −2 tests/unit/test_fuzzfailures.c
+16 −12 tests/unit/test_logger.c
+3 −0 tests/unit/test_modulefinder.c
+3 −8 tests/unit/test_mpack.c
+3 −3 tests/unit/test_options.c
+18 −7 tests/unit/test_path.c
+84 −70 tests/unit/test_sampling.c
+2 −2 tests/unit/test_session.c
+3 −0 tests/unit/test_symbolizer.c
+17 −17 tests/unit/test_tracing.c
+3 −3 tests/unit/test_uninit.c
+4 −0 tests/unit/test_unwinder.c
+2 −0 tests/unit/test_utils.c
+3 −0 tests/unit/test_value.c
+1 −1 tests/win_utils.py
+4 −0 vendor/acutest.h
6 changes: 3 additions & 3 deletions samples/Sentry.Samples.Maui/Sentry.Samples.Maui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
On Mac, we'll also build for iOS and MacCatalyst.
On Windows, we'll also build for Windows 10.
-->
<TargetFrameworks>$(TargetFrameworks);net9.0-android35.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0;net9.0-ios18.0;net9.0-maccatalyst18.0</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios18.0;net9.0-maccatalyst18.0</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworks);net9.0-android</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('OSX'))">$(TargetFrameworks);net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<RootNamespace>Sentry.Samples.Maui</RootNamespace>
<UseMaui>true</UseMaui>
Expand Down
31 changes: 31 additions & 0 deletions src/Sentry.Maui/Internal/IMauiPageEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Sentry.Maui.Internal;

/// <summary>
/// Allows you to receive MAUI page level events without hooking (this list is NOT exhaustive at this time)
/// </summary>
public interface IMauiPageEventHandler
{
/// <summary>
/// Page.OnAppearing
/// </summary>
/// <param name="page"></param>
public void OnAppearing(Page page);

/// <summary>
/// Page.OnDisappearing
/// </summary>
/// <param name="page"></param>
public void OnDisappearing(Page page);

/// <summary>
/// Page.OnNavigatedTo
/// </summary>
/// <param name="page"></param>
public void OnNavigatedTo(Page page);

/// <summary>
/// Page.OnNavigatedFrom
/// </summary>
/// <param name="page"></param>
public void OnNavigatedFrom(Page page);
}
11 changes: 9 additions & 2 deletions src/Sentry.Maui/Internal/MauiButtonEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace Sentry.Maui.Internal;

/// <inheritdoc />
public class MauiButtonEventsBinder : IMauiElementEventBinder
public class MauiButtonEventsBinder(IHub hub) : IMauiElementEventBinder
{
private Action<BreadcrumbEvent>? addBreadcrumbCallback;

Expand Down Expand Up @@ -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)));
Expand Down
27 changes: 24 additions & 3 deletions src/Sentry.Maui/Internal/MauiEventsBinder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System;
using Microsoft.Extensions.Options;
using Microsoft.Maui.Controls;
using Sentry.Internal;
using Sentry.Protocol;

namespace Sentry.Maui.Internal;

Expand All @@ -12,6 +16,7 @@ internal class MauiEventsBinder : IMauiEventsBinder
private readonly IHub _hub;
private readonly SentryMauiOptions _options;
private readonly IEnumerable<IMauiElementEventBinder> _elementEventBinders;
private readonly IEnumerable<IMauiPageEventHandler> _pageEventHandlers;

// https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
// https://github.com/getsentry/sentry/blob/master/static/app/types/breadcrumbs.tsx
Expand All @@ -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<SentryMauiOptions> options, IEnumerable<IMauiElementEventBinder> elementEventBinders)

public MauiEventsBinder(IHub hub, IOptions<SentryMauiOptions> options, IEnumerable<IMauiElementEventBinder> elementEventBinders, IEnumerable<IMauiPageEventHandler> pageEventHandlers)
{
_hub = hub;
_options = options.Value;
_elementEventBinders = elementEventBinders;
_pageEventHandlers = pageEventHandlers;
}

public void HandleApplicationEvents(Application application, bool bind = true)
Expand Down Expand Up @@ -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) =>
Expand Down Expand Up @@ -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<IMauiPageEventHandler> action)
{
foreach (var handler in _pageEventHandlers)
action(handler); // TODO: try/catch in case of user code?
}
}
79 changes: 79 additions & 0 deletions src/Sentry.Maui/Internal/TtdMauiPageEventHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Sentry.Internal;
using Sentry.Internal.Extensions;

namespace Sentry.Maui.Internal;

/// <summary>
/// Time-to-(initial/full)-display page event handler
/// https://docs.sentry.io/product/insights/mobile/mobile-vitals/
/// </summary>
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;

/// <inheritdoc />
public async void OnAppearing(Page page)
{
if (_ttidRan && StartupTimestamp != null)
return;

// if (Interlocked.Exchange<bool>(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) { }
}

5 changes: 5 additions & 0 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +44,8 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder, string dsn)
public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
Action<SentryMauiOptions>? configureOptions)
{
// we set this as early as possible and during the DI phase
TtdMauiPageEventHandler.StartupTimestamp = Stopwatch.GetTimestamp();
var services = builder.Services;

if (configureOptions != null)
Expand All @@ -59,6 +63,7 @@ public static MauiAppBuilder UseSentry(this MauiAppBuilder builder,
services.AddSingleton<IMauiElementEventBinder, MauiImageButtonEventsBinder>();
services.TryAddSingleton<IMauiEventsBinder, MauiEventsBinder>();

services.AddSingleton<IMauiPageEventHandler, TtdMauiPageEventHandler>();
services.AddSentry<SentryMauiOptions>();

builder.RegisterMauiEventsBinder();
Expand Down
4 changes: 2 additions & 2 deletions src/Sentry/ISentryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public interface ISentryClient
/// </summary>
/// <remarks>
/// Note: this method is NOT meant to be called from user code!
/// Instead, call <see cref="ISpan.Finish()"/> on the transaction.
/// Instead, call <see cref="ISpan.Finish(DateTimeOffset?)"/> on the transaction.
/// </remarks>
/// <param name="transaction">The transaction.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
Expand All @@ -59,7 +59,7 @@ public interface ISentryClient
/// </summary>
/// <remarks>
/// Note: this method is NOT meant to be called from user code!
/// Instead, call <see cref="ISpan.Finish()"/> on the transaction.
/// Instead, call <see cref="ISpan.Finish(DateTimeOffset?)"/> on the transaction.
/// </remarks>
/// <param name="transaction">The transaction.</param>
/// <param name="scope">The scope to be applied to the transaction</param>
Expand Down
25 changes: 15 additions & 10 deletions src/Sentry/ISpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ namespace Sentry;
/// </summary>
public interface ISpan : ISpanData
{
/// <summary>
/// When the status of the span changes, this event will fire
/// </summary>
public event EventHandler<SpanStatus?>? StatusChanged;

/// <summary>
/// Span description.
/// </summary>
Expand All @@ -28,27 +33,27 @@ public interface ISpan : ISpanData
/// <summary>
/// Starts a child span.
/// </summary>
public ISpan StartChild(string operation);
public ISpan StartChild(string operation, DateTimeOffset? startTime = null);

/// <summary>
/// Finishes the span.
/// </summary>
public void Finish();
/// </summary>`
public void Finish(DateTimeOffset? timestamp = null);

/// <summary>
/// Finishes the span with the specified status.
/// </summary>
public void Finish(SpanStatus status);
public void Finish(SpanStatus status, DateTimeOffset? timestamp = null);

/// <summary>
/// Finishes the span with the specified exception and status.
/// </summary>
public void Finish(Exception exception, SpanStatus status);
public void Finish(Exception exception, SpanStatus status, DateTimeOffset? timestamp = null);

/// <summary>
/// Finishes the span with the specified exception and automatically inferred status.
/// </summary>
public void Finish(Exception exception);
public void Finish(Exception exception, DateTimeOffset? timestamp = null);
}

/// <summary>
Expand All @@ -60,18 +65,18 @@ public static class SpanExtensions
/// <summary>
/// Starts a child span.
/// </summary>
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;
Expand Down
16 changes: 11 additions & 5 deletions src/Sentry/Internal/NoOpSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ public string Operation
set { }
}

public event EventHandler<SpanStatus?>? StatusChanged
{
add { }
remove { }
}

public string? Description
{
get => default;
Expand All @@ -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)
{
}

Expand Down
Loading