Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
20612cd
Fix TTID/TTFD not being created for root
buenaflor Jul 22, 2025
4654477
Update
buenaflor Jul 22, 2025
26ad5f9
Update test
buenaflor Jul 22, 2025
e585f0e
Fix analyze
buenaflor Jul 22, 2025
9759b94
Merge branch 'main' into fix/web-ttid-for-root
buenaflor Jul 23, 2025
524bf72
Update
buenaflor Jul 23, 2025
5908292
Update
buenaflor Jul 23, 2025
3e8e8a1
Update test
buenaflor Jul 23, 2025
1249055
Update test
buenaflor Jul 23, 2025
e0febd6
Update
buenaflor Jul 23, 2025
920cd13
Update
buenaflor Jul 23, 2025
b44fc1a
Update
buenaflor Jul 23, 2025
f5cf0f9
Update test
buenaflor Jul 23, 2025
6cb5a00
Update assert
buenaflor Jul 23, 2025
9588357
Set origin to native app start as well
buenaflor Jul 24, 2025
847ba72
Update
buenaflor Jul 24, 2025
f046892
Update
buenaflor Jul 24, 2025
c3a2586
Update
buenaflor Jul 24, 2025
030cff0
Update
buenaflor Jul 24, 2025
ccf8649
Update
buenaflor Jul 24, 2025
4cdba90
Merge branch 'main' into fix/web-ttid-for-root
buenaflor Jul 24, 2025
5fcfc50
Add comment
buenaflor Jul 24, 2025
27648fb
Update
buenaflor Jul 24, 2025
1b80841
Update
buenaflor Jul 24, 2025
d183fd2
Update
buenaflor Jul 24, 2025
d4595eb
Review
buenaflor Jul 24, 2025
6c4b5b3
Review
buenaflor Jul 24, 2025
5b072d3
Review
buenaflor Jul 24, 2025
38b3b80
Review
buenaflor Jul 24, 2025
57722c4
Update
buenaflor Jul 25, 2025
fda1bd1
Update
buenaflor Jul 25, 2025
0740f6f
Update
buenaflor Jul 25, 2025
d684ddc
Update
buenaflor Jul 25, 2025
def5eaf
Analyze
buenaflor Jul 25, 2025
52c537c
Update
buenaflor Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
### Fixes

- Debug meta not loaded for split debug info only builds ([#3104](https://github.com/getsentry/sentry-dart/pull/3104))
- TTID/TTFD root transactions ([#3099](https://github.com/getsentry/sentry-dart/pull/3099))
- Web, Linux and Windows now create a UI transaction for the root page
- iOS, Android now correctly create idle transactions

### Dependencies

Expand Down
195 changes: 195 additions & 0 deletions dart/test/sentry_client_sdk_lifecycle_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import 'package:sentry/sentry.dart';
import 'package:sentry/src/platform/mock_platform.dart';
import 'package:sentry/src/sentry_tracer.dart';
import 'package:test/test.dart';

import 'mocks/mock_client_report_recorder.dart';
import 'mocks/mock_log_batcher.dart';
import 'mocks/mock_transport.dart';
import 'sentry_client_test.dart';
import 'test_utils.dart';
import 'utils/url_details_test.dart';

void main() {
group('SDK lifecycle callbacks', () {
late Fixture fixture;

setUp(() => fixture = Fixture());

group('Logs', () {
SentryLog givenLog() {
return SentryLog(
timestamp: DateTime.now(),
traceId: SentryId.newId(),
level: SentryLogLevel.info,
body: 'test',
attributes: {
'attribute': SentryLogAttribute.string('value'),
},
);
}

test('captureLog triggers OnBeforeCaptureLog', () async {
fixture.options.enableLogs = true;
fixture.options.environment = 'test-environment';
fixture.options.release = 'test-release';

final log = givenLog();

final scope = Scope(fixture.options);
final span = MockSpan();
scope.span = span;

final client = fixture.getSut();
fixture.options.logBatcher = MockLogBatcher();

client.lifeCycleRegistry.registerCallback<OnBeforeCaptureLog>((event) {
event.log.attributes['test'] =
SentryLogAttribute.string('test-value');
});

await client.captureLog(log, scope: scope);

final mockLogBatcher = fixture.options.logBatcher as MockLogBatcher;
expect(mockLogBatcher.addLogCalls.length, 1);
final capturedLog = mockLogBatcher.addLogCalls.first;

expect(capturedLog.attributes['test']?.value, "test-value");
expect(capturedLog.attributes['test']?.type, 'string');
});
});

group('SentryEvent', () {
test('captureEvent triggers OnBeforeSendEvent', () async {
fixture.options.enableLogs = true;
fixture.options.environment = 'test-environment';
fixture.options.release = 'test-release';

final event = SentryEvent();

final scope = Scope(fixture.options);
final span = MockSpan();
scope.span = span;

final client = fixture.getSut();
fixture.options.logBatcher = MockLogBatcher();

client.lifeCycleRegistry.registerCallback<OnBeforeSendEvent>((event) {
event.event.release = '999';
});

await client.captureEvent(event, scope: scope);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

expect(capturedEvent.release, '999');
});
});
});
}

class Fixture {
final recorder = MockClientReportRecorder();
final transport = MockTransport();

final options = defaultTestOptions()
..platform = MockPlatform.iOS()
..groupExceptions = true;

late SentryTransactionContext _context;
late SentryTracer tracer;

SentryLevel? loggedLevel;
Object? loggedException;

SentryClient getSut({
bool sendDefaultPii = false,
bool attachStacktrace = true,
bool attachThreads = false,
double? sampleRate,
BeforeSendCallback? beforeSend,
BeforeSendTransactionCallback? beforeSendTransaction,
BeforeSendCallback? beforeSendFeedback,
EventProcessor? eventProcessor,
bool provideMockRecorder = true,
bool debug = false,
Transport? transport,
}) {
options.tracesSampleRate = 1.0;
options.sendDefaultPii = sendDefaultPii;
options.attachStacktrace = attachStacktrace;
options.attachThreads = attachThreads;
options.sampleRate = sampleRate;
options.beforeSend = beforeSend;
options.beforeSendTransaction = beforeSendTransaction;
options.beforeSendFeedback = beforeSendFeedback;
options.debug = debug;
options.log = mockLogger;

if (eventProcessor != null) {
options.addEventProcessor(eventProcessor);
}

// Internally also creates a SentryClient instance
final hub = Hub(options);
_context = SentryTransactionContext(
'name',
'op',
);
tracer = SentryTracer(_context, hub);

// Reset transport
options.transport = transport ?? this.transport;

// Again create SentryClient instance
final client = SentryClient(options);

if (provideMockRecorder) {
options.recorder = recorder;
}
return client;
}

Future<SentryEvent?> droppingBeforeSend(SentryEvent event, Hint hint) async {
return null;
}

SentryTransaction fakeTransaction() {
return SentryTransaction(
tracer,
sdk: SdkVersion(name: 'sdk1', version: '1.0.0'),
breadcrumbs: [],
);
}

SentryEvent fakeFeedbackEvent() {
return SentryEvent(
type: 'feedback',
contexts: Contexts(feedback: fakeFeedback()),
level: SentryLevel.info,
);
}

SentryFeedback fakeFeedback() {
return SentryFeedback(
message: 'fixture-message',
contactEmail: 'fixture-contactEmail',
name: 'fixture-name',
replayId: 'fixture-replayId',
url: "https://fixture-url.com",
associatedEventId: SentryId.fromId('1d49af08b6e2c437f9052b1ecfd83dca'),
);
}

void mockLogger(
SentryLevel level,
String message, {
String? logger,
Object? exception,
StackTrace? stackTrace,
}) {
loggedLevel = level;
loggedException = exception;
}
}
74 changes: 74 additions & 0 deletions flutter/lib/src/integrations/generic_app_start_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// ignore_for_file: invalid_use_of_internal_member

import 'package:meta/meta.dart';

import '../../sentry_flutter.dart';
import '../frame_callback_handler.dart';

// TODO(buenaflor): marking this internal until we can find a robust way to unify the TTID/TTFD implementation as currently it is very fragmented.

/// A fallback app–start integration for platforms without built-in app-start timing.
///
/// The Sentry Cocoa and Android SDKs include calls to capture the
/// exact application start timestamp. Other platforms—such as web, desktop,
/// or any SDK that doesn’t (yet) expose app-start instrumentation can use this
/// integration as a reasonable alternative. It measures the duration from
/// integration call to the first completed frame.
@internal
class GenericAppStartIntegration extends Integration<SentryFlutterOptions> {
GenericAppStartIntegration([FrameCallbackHandler? frameHandler])
: _framesHandler = frameHandler ?? DefaultFrameCallbackHandler();

final FrameCallbackHandler _framesHandler;

static const String integrationName = 'GenericAppStart';

@override
void call(Hub hub, SentryFlutterOptions options) {
if (!options.isTracingEnabled()) return;

final transactionContext = SentryTransactionContext(
'root /',
SentrySpanOperations.uiLoad,
origin: SentryTraceOrigins.autoUiTimeToDisplay,
);

final startTimeStamp = options.clock();
final transaction = hub.startTransactionWithContext(
transactionContext,
startTimestamp: startTimeStamp,
waitForChildren: true,
autoFinishAfter: Duration(seconds: 3),
bindToScope: true,
trimEnd: true,
);

options.timeToDisplayTracker.transactionId = transactionContext.spanId;

_framesHandler.addPostFrameCallback((_) async {
try {
final endTimestamp = options.clock();
await options.timeToDisplayTracker.track(
transaction,
ttidEndTimestamp: endTimestamp,
);

// Note: we do not set app start transaction measurements (yet) on purpose
// This integration is used for TTID/TTFD mainly
// However this may change in the future.
} catch (exception, stackTrace) {
options.log(
SentryLevel.error,
'An exception occurred while executing the $GenericAppStartIntegration',
exception: exception,
stackTrace: stackTrace,
);
if (options.automatedTestMode) {
rethrow;
}
}
});

options.sdk.addIntegration(integrationName);
}
}
33 changes: 12 additions & 21 deletions flutter/lib/src/integrations/native_app_start_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,10 @@ class NativeAppStartHandler {
final rootScreenTransaction = _hub.startTransactionWithContext(
context,
startTimestamp: appStartInfo.start,
);

// Bind to scope if null
await _hub.configureScope((scope) {
scope.span ??= rootScreenTransaction;
});

await options.timeToDisplayTracker.track(
rootScreenTransaction,
ttidEndTimestamp: appStartInfo.end,
waitForChildren: true,
autoFinishAfter: Duration(seconds: 3),
bindToScope: true,
trimEnd: true,
);

SentryTracer sentryTracer;
Expand All @@ -63,20 +57,17 @@ class NativeAppStartHandler {
return;
}

// Enrich Transaction
// We need to add the measurements before we add the child spans
// If the child span finish the transaction will finish and then we cannot add measurements
// TODO(buenaflor): eventually we can move this to the onFinish callback
SentryMeasurement? measurement = appStartInfo.toMeasurement();
sentryTracer.measurements[measurement.name] = appStartInfo.toMeasurement();
await _attachAppStartSpans(appStartInfo, sentryTracer);

// Remove from scope
await _hub.configureScope((scope) {
if (scope.span == rootScreenTransaction) {
scope.span = null;
}
});

// Finish Transaction
await rootScreenTransaction.finish(endTimestamp: appStartInfo.end);
await options.timeToDisplayTracker.track(
rootScreenTransaction,
ttidEndTimestamp: appStartInfo.end,
);
await _attachAppStartSpans(appStartInfo, sentryTracer);
}

_AppStartInfo? _infoNativeAppStart(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// ignore_for_file: invalid_use_of_internal_member

import 'dart:ui';

import 'package:meta/meta.dart';
Expand Down Expand Up @@ -31,8 +33,8 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
// Create context early so we have an id to refernce for reporting full display
final context = SentryTransactionContext(
'root /',
// ignore: invalid_use_of_internal_member
SentrySpanOperations.uiLoad,
origin: SentryTraceOrigins.autoUiTimeToDisplay,
);
options.timeToDisplayTracker.transactionId = context.spanId;

Expand All @@ -45,7 +47,6 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
_allowProcessing = false;

try {
// ignore: invalid_use_of_internal_member
final appStartEnd = DateTime.fromMicrosecondsSinceEpoch(timings.first
.timestampInMicroseconds(FramePhase.rasterFinishWallTime));
await _nativeAppStartHandler.call(
Expand Down
4 changes: 2 additions & 2 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
final routeName = _getRouteName(route) ?? _currentRouteName;
final arguments = route?.settings.arguments;

final isRoot = routeName ==
'/'; // Root transaction is already created by the app start integration.
// Skip root - app start integrations create TTID/TTFD for root
final isRoot = routeName == '/';
if (!_enableAutoTransactions || routeName == null || isRoot) {
return;
}
Expand Down
Loading
Loading