diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 080956591b..c9534d1a0e 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -502,6 +502,7 @@ class Hub { Future captureTransaction( SentryTransaction transaction, { SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) async { var sentryId = SentryId.empty(); @@ -538,6 +539,7 @@ class Hub { transaction, scope: item.scope, traceContext: traceContext, + profileInfo: profileInfo, ); } catch (exception, stackTrace) { _options.logger( diff --git a/dart/lib/src/hub_adapter.dart b/dart/lib/src/hub_adapter.dart index aafd186d17..52f24d12b5 100644 --- a/dart/lib/src/hub_adapter.dart +++ b/dart/lib/src/hub_adapter.dart @@ -99,6 +99,7 @@ class HubAdapter implements Hub { Future captureTransaction( SentryTransaction transaction, { SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) => Sentry.currentHub.captureTransaction( transaction, diff --git a/dart/lib/src/noop_hub.dart b/dart/lib/src/noop_hub.dart index 38914b9f5e..9369f13b0c 100644 --- a/dart/lib/src/noop_hub.dart +++ b/dart/lib/src/noop_hub.dart @@ -80,6 +80,7 @@ class NoOpHub implements Hub { Future captureTransaction( SentryTransaction transaction, { SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) async => SentryId.empty(); diff --git a/dart/lib/src/profiling.dart b/dart/lib/src/profiling.dart index e6dd4eb144..9830bdc0ec 100644 --- a/dart/lib/src/profiling.dart +++ b/dart/lib/src/profiling.dart @@ -6,17 +6,17 @@ import '../sentry.dart'; @internal abstract class ProfilerFactory { - Profiler startProfiling(SentryTransactionContext context); + Profiler? startProfiling(SentryTransactionContext context); } @internal abstract class Profiler { - Future finishFor(SentryTransaction transaction); + Future finishFor(SentryTransaction transaction); void dispose(); } // See https://develop.sentry.dev/sdk/profiles/ @internal abstract class ProfileInfo { - FutureOr asEnvelopeItem(); + SentryEnvelopeItem asEnvelopeItem(); } diff --git a/dart/lib/src/protocol/sentry_transaction.dart b/dart/lib/src/protocol/sentry_transaction.dart index 606c15e8d9..86f04f1da4 100644 --- a/dart/lib/src/protocol/sentry_transaction.dart +++ b/dart/lib/src/protocol/sentry_transaction.dart @@ -15,9 +15,6 @@ class SentryTransaction extends SentryEvent { late final Map measurements; late final SentryTransactionInfo? transactionInfo; - @internal - late final ProfileInfo? profileInfo; - SentryTransaction( this._tracer, { SentryId? eventId, diff --git a/dart/lib/src/sentry_client.dart b/dart/lib/src/sentry_client.dart index 09f98e4d02..023a0674c1 100644 --- a/dart/lib/src/sentry_client.dart +++ b/dart/lib/src/sentry_client.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:meta/meta.dart'; +import 'profiling.dart'; import 'sentry_attachment/sentry_attachment.dart'; import 'event_processor.dart'; @@ -284,6 +285,7 @@ class SentryClient { SentryTransaction transaction, { Scope? scope, SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) async { SentryTransaction? preparedTransaction = _prepareEvent(transaction) as SentryTransaction; @@ -331,6 +333,9 @@ class SentryClient { traceContext: traceContext, attachments: attachments, ); + if (profileInfo != null) { + envelope.items.add(profileInfo.asEnvelopeItem()); + } final id = await captureEnvelope(envelope); return id ?? SentryId.empty(); diff --git a/dart/lib/src/sentry_envelope_item.dart b/dart/lib/src/sentry_envelope_item.dart index ce08fbb56d..5eb73443d3 100644 --- a/dart/lib/src/sentry_envelope_item.dart +++ b/dart/lib/src/sentry_envelope_item.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:meta/meta.dart'; + import 'client_reports/client_report.dart'; import 'protocol.dart'; import 'utils.dart'; diff --git a/dart/lib/src/sentry_item_type.dart b/dart/lib/src/sentry_item_type.dart index c74b6b6049..6215cbb78f 100644 --- a/dart/lib/src/sentry_item_type.dart +++ b/dart/lib/src/sentry_item_type.dart @@ -4,5 +4,6 @@ class SentryItemType { static const String attachment = 'attachment'; static const String transaction = 'transaction'; static const String clientReport = 'client_report'; + static const String profile = 'profile'; static const String unknown = '__unknown__'; } diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index d2a7fe7458..d3e7b2302e 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -141,15 +141,14 @@ class SentryTracer extends ISentrySpan { final transaction = SentryTransaction(this); transaction.measurements.addAll(_measurements); - if (profiler != null) { - if (status == null || status == SpanStatus.ok()) { - transaction.profileInfo = await profiler?.finishFor(transaction); - } - } + final profileInfo = (status == null || status == SpanStatus.ok()) + ? await profiler?.finishFor(transaction) + : null; await _hub.captureTransaction( transaction, traceContext: traceContext(), + profileInfo: profileInfo, ); } finally { profiler?.dispose(); diff --git a/dart/test/mocks.mocks.dart b/dart/test/mocks.mocks.dart index 8f8f67a5ae..249f7e4862 100644 --- a/dart/test/mocks.mocks.dart +++ b/dart/test/mocks.mocks.dart @@ -6,8 +6,8 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:sentry/sentry.dart' as _i3; -import 'package:sentry/src/profiling.dart' as _i2; +import 'package:sentry/sentry.dart' as _i2; +import 'package:sentry/src/profiling.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -20,29 +20,9 @@ import 'package:sentry/src/profiling.dart' as _i2; // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeProfiler_0 extends _i1.SmartFake implements _i2.Profiler { - _FakeProfiler_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProfileInfo_1 extends _i1.SmartFake implements _i2.ProfileInfo { - _FakeProfileInfo_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeSentryEnvelopeItem_2 extends _i1.SmartFake - implements _i3.SentryEnvelopeItem { - _FakeSentryEnvelopeItem_2( +class _FakeSentryEnvelopeItem_0 extends _i1.SmartFake + implements _i2.SentryEnvelopeItem { + _FakeSentryEnvelopeItem_0( Object parent, Invocation parentInvocation, ) : super( @@ -54,51 +34,36 @@ class _FakeSentryEnvelopeItem_2 extends _i1.SmartFake /// A class which mocks [ProfilerFactory]. /// /// See the documentation for Mockito's code generation for more information. -class MockProfilerFactory extends _i1.Mock implements _i2.ProfilerFactory { +class MockProfilerFactory extends _i1.Mock implements _i3.ProfilerFactory { MockProfilerFactory() { _i1.throwOnMissingStub(this); } @override - _i2.Profiler startProfiling(_i3.SentryTransactionContext? context) => - (super.noSuchMethod( - Invocation.method( - #startProfiling, - [context], - ), - returnValue: _FakeProfiler_0( - this, - Invocation.method( - #startProfiling, - [context], - ), - ), - ) as _i2.Profiler); + _i3.Profiler? startProfiling(_i2.SentryTransactionContext? context) => + (super.noSuchMethod(Invocation.method( + #startProfiling, + [context], + )) as _i3.Profiler?); } /// A class which mocks [Profiler]. /// /// See the documentation for Mockito's code generation for more information. -class MockProfiler extends _i1.Mock implements _i2.Profiler { +class MockProfiler extends _i1.Mock implements _i3.Profiler { MockProfiler() { _i1.throwOnMissingStub(this); } @override - _i4.Future<_i2.ProfileInfo> finishFor(_i3.SentryTransaction? transaction) => + _i4.Future<_i3.ProfileInfo?> finishFor(_i2.SentryTransaction? transaction) => (super.noSuchMethod( Invocation.method( #finishFor, [transaction], ), - returnValue: _i4.Future<_i2.ProfileInfo>.value(_FakeProfileInfo_1( - this, - Invocation.method( - #finishFor, - [transaction], - ), - )), - ) as _i4.Future<_i2.ProfileInfo>); + returnValue: _i4.Future<_i3.ProfileInfo?>.value(), + ) as _i4.Future<_i3.ProfileInfo?>); @override void dispose() => super.noSuchMethod( Invocation.method( @@ -112,24 +77,23 @@ class MockProfiler extends _i1.Mock implements _i2.Profiler { /// A class which mocks [ProfileInfo]. /// /// See the documentation for Mockito's code generation for more information. -class MockProfileInfo extends _i1.Mock implements _i2.ProfileInfo { +class MockProfileInfo extends _i1.Mock implements _i3.ProfileInfo { MockProfileInfo() { _i1.throwOnMissingStub(this); } @override - _i4.FutureOr<_i3.SentryEnvelopeItem> asEnvelopeItem() => (super.noSuchMethod( + _i2.SentryEnvelopeItem asEnvelopeItem() => (super.noSuchMethod( Invocation.method( #asEnvelopeItem, [], ), - returnValue: - _i4.Future<_i3.SentryEnvelopeItem>.value(_FakeSentryEnvelopeItem_2( + returnValue: _FakeSentryEnvelopeItem_0( this, Invocation.method( #asEnvelopeItem, [], ), - )), - ) as _i4.FutureOr<_i3.SentryEnvelopeItem>); + ), + ) as _i2.SentryEnvelopeItem); } diff --git a/dart/test/mocks/mock_hub.dart b/dart/test/mocks/mock_hub.dart index d8b4d7384b..c7d429de81 100644 --- a/dart/test/mocks/mock_hub.dart +++ b/dart/test/mocks/mock_hub.dart @@ -1,5 +1,6 @@ import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; +import 'package:sentry/src/profiling.dart'; import '../mocks.dart'; import 'mock_sentry_client.dart'; @@ -110,6 +111,7 @@ class MockHub with NoSuchMethodProvider implements Hub { Future captureTransaction( SentryTransaction transaction, { SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) async { captureTransactionCalls .add(CaptureTransactionCall(transaction, traceContext)); diff --git a/dart/test/mocks/mock_sentry_client.dart b/dart/test/mocks/mock_sentry_client.dart index 7b08250193..db8aad08c4 100644 --- a/dart/test/mocks/mock_sentry_client.dart +++ b/dart/test/mocks/mock_sentry_client.dart @@ -1,4 +1,5 @@ import 'package:sentry/sentry.dart'; +import 'package:sentry/src/profiling.dart'; import 'no_such_method_provider.dart'; @@ -84,6 +85,7 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient { SentryTransaction transaction, { Scope? scope, SentryTraceContextHeader? traceContext, + ProfileInfo? profileInfo, }) async { captureTransactionCalls .add(CaptureTransactionCall(transaction, traceContext)); diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 6d657a589e..bb4b58a01a 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -48,6 +48,7 @@ Future setupSentry(AppRunner appRunner, String dsn, await SentryFlutter.init((options) { options.dsn = exampleDsn; options.tracesSampleRate = 1.0; + options.profilesSampleRate = 1.0; options.reportPackages = false; options.addInAppInclude('sentry_flutter_example'); options.considerInAppFramesByDefault = false; diff --git a/flutter/lib/src/profiling.dart b/flutter/lib/src/profiling.dart new file mode 100644 index 0000000000..c163ee7582 --- /dev/null +++ b/flutter/lib/src/profiling.dart @@ -0,0 +1,110 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +// ignore: implementation_imports +import 'package:sentry/src/profiling.dart'; +// ignore: implementation_imports +import 'package:sentry/src/sentry_envelope_item_header.dart'; +// ignore: implementation_imports +import 'package:sentry/src/sentry_item_type.dart'; + +import 'sentry_native.dart'; + +// ignore: invalid_use_of_internal_member +class NativeProfilerFactory implements ProfilerFactory { + final SentryNative _native; + + NativeProfilerFactory(this._native); + + static void attachTo(Hub hub) { + // ignore: invalid_use_of_internal_member + final options = hub.options; + + // ignore: invalid_use_of_internal_member + if ((options.profilesSampleRate ?? 0.0) <= 0.0) { + return; + } + + if (options.platformChecker.isWeb) { + return; + } + + if (Platform.isMacOS || Platform.isIOS) { + // ignore: invalid_use_of_internal_member + hub.profilerFactory = NativeProfilerFactory(SentryNative()); + } + } + + @override + NativeProfiler? startProfiling(SentryTransactionContext context) { + if (context.traceId == SentryId.empty()) { + return null; + } + + final startTime = _native.startProfiling(context.traceId); + + // TODO we cannot await the future returned by a method channel because + // startTransaction() is synchronous. In order to make this code fully + // synchronous and actually start the profiler, we need synchronous FFI + // calls, see https://github.com/getsentry/sentry-dart/issues/1444 + // For now, return immediately even though the profiler may not have started yet... + return NativeProfiler(_native, startTime, context.traceId); + } +} + +// ignore: invalid_use_of_internal_member +class NativeProfiler implements Profiler { + final SentryNative _native; + final Future _startTime; + final SentryId _traceId; + + NativeProfiler(this._native, this._startTime, this._traceId); + + @override + void dispose() { + // TODO expose in the cocoa SDK + // _startTime.then((_) => _native.discardProfiling(this._traceId)); + } + + @override + Future finishFor(SentryTransaction transaction) async { + final starTime = await _startTime; + if (starTime == null) { + return null; + } + + final payload = await _native.collectProfile(_traceId, starTime); + if (payload == null) { + return null; + } + + payload["transaction"] = { + "id": transaction.eventId.toString(), + "trace_id": _traceId.toString(), + "name": transaction.transaction, + // "active_thread_id" : [transaction.trace.transactionContext sentry_threadInfo].threadId + }; + payload["timestamp"] = transaction.startTimestamp.toIso8601String(); + return NativeProfileInfo(payload); + } +} + +// ignore: invalid_use_of_internal_member +class NativeProfileInfo implements ProfileInfo { + final Map _payload; + // ignore: invalid_use_of_internal_member + late final List _data = utf8JsonEncoder.convert(_payload); + + NativeProfileInfo(this._payload); + + @override + SentryEnvelopeItem asEnvelopeItem() { + final header = SentryEnvelopeItemHeader( + SentryItemType.profile, + () => Future.value(_data.length), + contentType: 'application/json', + ); + return SentryEnvelopeItem(header, () => Future.value(_data)); + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 13aa7cc552..73862a91c4 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -11,6 +11,7 @@ import 'event_processor/flutter_exception_event_processor.dart'; import 'event_processor/platform_exception_event_processor.dart'; import 'integrations/screenshot_integration.dart'; import 'native_scope_observer.dart'; +import 'profiling.dart'; import 'renderer/renderer.dart'; import 'sentry_native.dart'; import 'sentry_native_channel.dart'; @@ -81,9 +82,7 @@ mixin SentryFlutter { await _initDefaultValues(flutterOptions, channel); await Sentry.init( - (options) async { - await optionsConfiguration(options as SentryFlutterOptions); - }, + (options) => optionsConfiguration(options as SentryFlutterOptions), appRunner: appRunner, // ignore: invalid_use_of_internal_member options: flutterOptions, @@ -92,6 +91,9 @@ mixin SentryFlutter { // ignore: invalid_use_of_internal_member runZonedGuardedOnError: runZonedGuardedOnError, ); + + // ignore: invalid_use_of_internal_member + NativeProfilerFactory.attachTo(Sentry.currentHub); } static Future _initDefaultValues( diff --git a/flutter/lib/src/sentry_native.dart b/flutter/lib/src/sentry_native.dart index d6012d008f..67cd7ac067 100644 --- a/flutter/lib/src/sentry_native.dart +++ b/flutter/lib/src/sentry_native.dart @@ -93,11 +93,12 @@ class SentryNative { } Future startProfiling(SentryId traceId) async { - return await _nativeChannel?.startProfiling(traceId); + return _nativeChannel?.startProfiling(traceId); } - Future collectProfile(SentryId traceId, int startTimeNs) async { - return await _nativeChannel?.collectProfile(traceId, startTimeNs); + Future?> collectProfile( + SentryId traceId, int startTimeNs) async { + return _nativeChannel?.collectProfile(traceId, startTimeNs); } /// Reset state diff --git a/flutter/lib/src/sentry_native_channel.dart b/flutter/lib/src/sentry_native_channel.dart index db535012b2..8a555a6f6e 100644 --- a/flutter/lib/src/sentry_native_channel.dart +++ b/flutter/lib/src/sentry_native_channel.dart @@ -128,12 +128,11 @@ class SentryNativeChannel { } } - Future collectProfile(SentryId traceId, int startTimeNs) async { + Future?> collectProfile( + SentryId traceId, int startTimeNs) async { try { - return await _channel.invokeMethod('collectProfile', { - 'traceId': traceId.toString(), - 'startTime': startTimeNs - }) as Map?; + return await _channel.invokeMapMethod('collectProfile', + {'traceId': traceId.toString(), 'startTime': startTimeNs}); } catch (error, stackTrace) { _logError('collectProfile', error, stackTrace); return null;