Skip to content

Commit

Permalink
App start: Add spans to first transaction (#2009)
Browse files Browse the repository at this point in the history
* commit

* Update

* Remove print

* Remove comments

* Update

* Add linting

* Update CHANGELOG

* Update CHANGELOG.md

* Update naming

* Update naming

* Update naming

* Update description from first frame render to initial frame render

* update

* dart format

* Update comments

* Update

* Update

* Update

* Update

* Update

* Fix tests

* Fix test

* Add unused import

* Review improvements

* Update CHANGELOG

* Update CHANGELOG
  • Loading branch information
buenaflor authored May 3, 2024
1 parent a13cc88 commit 6575860
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 21 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# Changelog

## Unreleased

### Features

- Adds app start spans to first transaction ([#2009](https://github.com/getsentry/sentry-dart/pull/2009))

## 8.1.0

### Feature
### Features

- Set snapshot to `true` if stacktrace is not provided ([#2000](https://github.com/getsentry/sentry-dart/pull/2000))
- If the stacktrace is not provided, the Sentry SDK will fetch the current stacktrace via `StackTrace.current` and the snapshot will be set to `true` - **this may change the grouping behavior**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {

private var activity: WeakReference<Activity>? = null
private var framesTracker: ActivityFramesTracker? = null
private var pluginRegistrationTime: Long? = null

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
pluginRegistrationTime = System.currentTimeMillis()

context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "sentry_flutter")
channel.setMethodCallHandler(this)
Expand Down Expand Up @@ -137,6 +140,7 @@ class SentryFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
} else {
val appStartTimeMillis = DateUtils.nanosToMillis(appStartTime.nanoTimestamp().toDouble())
val item = mapOf<String, Any?>(
"pluginRegistrationTime" to pluginRegistrationTime,
"appStartTime" to appStartTimeMillis,
"isColdStart" to isColdStart,
)
Expand Down
5 changes: 5 additions & 0 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
#endif
}

private static var pluginRegistrationTime: Int64 = 0

public static func register(with registrar: FlutterPluginRegistrar) {
pluginRegistrationTime = Int64(Date().timeIntervalSince1970 * 1000)

#if os(iOS)
let channel = FlutterMethodChannel(name: "sentry_flutter", binaryMessenger: registrar.messenger())
#elseif os(macOS)
Expand Down Expand Up @@ -387,6 +391,7 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
let isColdStart = appStartMeasurement.type == .cold

let item: [String: Any] = [
"pluginRegistrationTime": SentryFlutterPluginApple.pluginRegistrationTime,
"appStartTime": appStartTime,
"isColdStart": isColdStart
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import 'dart:async';
// ignore_for_file: invalid_use_of_internal_member

import 'package:sentry/sentry.dart';
import 'dart:async';

import '../../sentry_flutter.dart';
import '../integrations/integrations.dart';
import '../native/sentry_native.dart';

// ignore: implementation_imports
import 'package:sentry/src/sentry_tracer.dart';

/// EventProcessor that enriches [SentryTransaction] objects with app start
/// measurement.
class NativeAppStartEventProcessor implements EventProcessor {
final SentryNative _native;
final Hub _hub;

NativeAppStartEventProcessor(this._native);
NativeAppStartEventProcessor(
this._native, {
Hub? hub,
}) : _hub = hub ?? HubAdapter();

@override
Future<SentryEvent?> apply(SentryEvent event, Hint hint) async {
Expand All @@ -25,6 +33,82 @@ class NativeAppStartEventProcessor implements EventProcessor {
event.measurements[measurement.name] = measurement;
_native.didAddAppStartMeasurement = true;
}

if (appStartInfo != null) {
await _attachAppStartSpans(appStartInfo, event.tracer);
}

return event;
}

Future<void> _attachAppStartSpans(
AppStartInfo appStartInfo, SentryTracer transaction) async {
final transactionTraceId = transaction.context.traceId;

final appStartSpan = await _createAndFinishSpan(
tracer: transaction,
operation: appStartInfo.appStartTypeOperation,
description: appStartInfo.appStartTypeDescription,
parentSpanId: transaction.context.spanId,
traceId: transactionTraceId,
startTimestamp: appStartInfo.start,
endTimestamp: appStartInfo.end);

final pluginRegistrationSpan = await _createAndFinishSpan(
tracer: transaction,
operation: appStartInfo.appStartTypeOperation,
description: appStartInfo.pluginRegistrationDescription,
parentSpanId: appStartSpan.context.spanId,
traceId: transactionTraceId,
startTimestamp: appStartInfo.start,
endTimestamp: appStartInfo.pluginRegistration);

final mainIsolateSetupSpan = await _createAndFinishSpan(
tracer: transaction,
operation: appStartInfo.appStartTypeOperation,
description: appStartInfo.mainIsolateSetupDescription,
parentSpanId: appStartSpan.context.spanId,
traceId: transactionTraceId,
startTimestamp: appStartInfo.pluginRegistration,
endTimestamp: appStartInfo.mainIsolateStart);

final firstFrameRenderSpan = await _createAndFinishSpan(
tracer: transaction,
operation: appStartInfo.appStartTypeOperation,
description: appStartInfo.firstFrameRenderDescription,
parentSpanId: appStartSpan.context.spanId,
traceId: transactionTraceId,
startTimestamp: appStartInfo.mainIsolateStart,
endTimestamp: appStartInfo.end);

transaction.children.addAll([
appStartSpan,
pluginRegistrationSpan,
mainIsolateSetupSpan,
firstFrameRenderSpan
]);
}

Future<SentrySpan> _createAndFinishSpan({
required SentryTracer tracer,
required String operation,
required String description,
required SpanId parentSpanId,
required SentryId traceId,
required DateTime startTimestamp,
required DateTime endTimestamp,
}) async {
final span = SentrySpan(
tracer,
SentrySpanContext(
operation: operation,
description: description,
parentSpanId: parentSpanId,
traceId: traceId,
),
_hub,
startTimestamp: startTimestamp);
await span.finish(endTimestamp: endTimestamp);
return span;
}
}
43 changes: 35 additions & 8 deletions flutter/lib/src/integrations/native_app_start_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
if (isIntegrationTest) {
final appStartInfo = AppStartInfo(AppStartType.cold,
start: DateTime.now(),
end: DateTime.now().add(const Duration(milliseconds: 100)));
end: DateTime.now().add(const Duration(milliseconds: 100)),
pluginRegistration:
DateTime.now().add(const Duration(milliseconds: 50)),
mainIsolateStart:
DateTime.now().add(const Duration(milliseconds: 60)));
setAppStartInfo(appStartInfo);
return;
}
Expand All @@ -67,16 +71,22 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
// We only assign the current time if it's not already set - this is useful in tests
// ignore: invalid_use_of_internal_member
_native.appStartEnd ??= options.clock();
final appStartEnd = _native.appStartEnd;
final appStartEndDateTime = _native.appStartEnd;
final nativeAppStart = await _native.fetchNativeAppStart();
final pluginRegistrationTime = nativeAppStart?.pluginRegistrationTime;
final mainIsolateStartDateTime = SentryFlutter.mainIsolateStartTime;

if (nativeAppStart == null || appStartEnd == null) {
if (nativeAppStart == null ||
appStartEndDateTime == null ||
pluginRegistrationTime == null) {
return;
}

final appStartDateTime = DateTime.fromMillisecondsSinceEpoch(
nativeAppStart.appStartTime.toInt());
final duration = appStartEnd.difference(appStartDateTime);
final duration = appStartEndDateTime.difference(appStartDateTime);
final pluginRegistrationDateTime =
DateTime.fromMillisecondsSinceEpoch(pluginRegistrationTime);

// We filter out app start more than 60s.
// This could be due to many different reasons.
Expand All @@ -93,9 +103,11 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {

final appStartInfo = AppStartInfo(
nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm,
start: DateTime.fromMillisecondsSinceEpoch(
nativeAppStart.appStartTime.toInt()),
end: appStartEnd);
start: appStartDateTime,
end: appStartEndDateTime,
pluginRegistration: pluginRegistrationDateTime,
mainIsolateStart: mainIsolateStartDateTime);

setAppStartInfo(appStartInfo);
});
}
Expand All @@ -109,16 +121,31 @@ class NativeAppStartIntegration extends Integration<SentryFlutterOptions> {
enum AppStartType { cold, warm }

class AppStartInfo {
AppStartInfo(this.type, {required this.start, required this.end});
AppStartInfo(this.type,
{required this.start,
required this.end,
required this.pluginRegistration,
required this.mainIsolateStart});

final AppStartType type;
final DateTime start;
final DateTime end;
final DateTime pluginRegistration;
final DateTime mainIsolateStart;

Duration get duration => end.difference(start);

SentryMeasurement toMeasurement() {
return type == AppStartType.cold
? SentryMeasurement.coldAppStart(duration)
: SentryMeasurement.warmAppStart(duration);
}

String get appStartTypeOperation => 'app.start.${type.name}';

String get appStartTypeDescription =>
type == AppStartType.cold ? 'Cold start' : 'Warm start';
final pluginRegistrationDescription = 'App start to plugin registration';
final mainIsolateSetupDescription = 'Main isolate setup';
final firstFrameRenderDescription = 'First frame render';
}
11 changes: 8 additions & 3 deletions flutter/lib/src/native/sentry_native.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,20 @@ class SentryNative {
}

class NativeAppStart {
NativeAppStart(this.appStartTime, this.isColdStart);
NativeAppStart(
{required this.appStartTime,
required this.pluginRegistrationTime,
required this.isColdStart});

double appStartTime;
int pluginRegistrationTime;
bool isColdStart;

factory NativeAppStart.fromJson(Map<String, dynamic> json) {
return NativeAppStart(
json['appStartTime'] as double,
json['isColdStart'] as bool,
appStartTime: json['appStartTime'] as double,
pluginRegistrationTime: json['pluginRegistrationTime'] as int,
isColdStart: json['isColdStart'] as bool,
);
}
}
Expand Down
5 changes: 5 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ typedef FlutterOptionsConfiguration = FutureOr<void> Function(
mixin SentryFlutter {
static const _channel = MethodChannel('sentry_flutter');

/// Represents the time when the main isolate is set up and ready to execute.
@internal
// ignore: invalid_use_of_internal_member
static DateTime mainIsolateStartTime = getUtcDateTime();

static Future<void> init(
FlutterOptionsConfiguration optionsConfiguration, {
AppRunner? appRunner,
Expand Down
Loading

0 comments on commit 6575860

Please sign in to comment.