diff --git a/dart/lib/src/hub.dart b/dart/lib/src/hub.dart index 88667aaa2d..2812dfca1e 100644 --- a/dart/lib/src/hub.dart +++ b/dart/lib/src/hub.dart @@ -455,7 +455,7 @@ class Hub { Profiler? profiler; if (_profilerFactory != null && _tracesSampler.sampleProfiling(samplingDecision)) { - profiler = _profilerFactory?.startProfiling(transactionContext); + profiler = _profilerFactory?.startProfiler(transactionContext); } final tracer = SentryTracer( diff --git a/dart/lib/src/profiling.dart b/dart/lib/src/profiling.dart index 9830bdc0ec..9bce57ffd7 100644 --- a/dart/lib/src/profiling.dart +++ b/dart/lib/src/profiling.dart @@ -6,7 +6,7 @@ import '../sentry.dart'; @internal abstract class ProfilerFactory { - Profiler? startProfiling(SentryTransactionContext context); + Profiler? startProfiler(SentryTransactionContext context); } @internal diff --git a/dart/lib/src/protocol/sentry_event.dart b/dart/lib/src/protocol/sentry_event.dart index e51441cd7d..32a76b9885 100644 --- a/dart/lib/src/protocol/sentry_event.dart +++ b/dart/lib/src/protocol/sentry_event.dart @@ -59,7 +59,7 @@ class SentryEvent with SentryEventLike { /// The ID Sentry.io assigned to the submitted event for future reference. final SentryId eventId; - /// A timestamp representing when the breadcrumb occurred. + /// A timestamp representing when the event occurred. final DateTime? timestamp; /// A string representing the platform the SDK is submitting from. This will be used by the Sentry interface to customize various components in the interface. diff --git a/dart/test/hub_test.dart b/dart/test/hub_test.dart index e257ec9be6..d2f1232b65 100644 --- a/dart/test/hub_test.dart +++ b/dart/test/hub_test.dart @@ -387,7 +387,7 @@ void main() { test('profiler is started according to the sampling rate', () async { final hub = fixture.getSut(); final factory = MockProfilerFactory(); - when(factory.startProfiling(fixture._context)).thenReturn(MockProfiler()); + when(factory.startProfiler(fixture._context)).thenReturn(MockProfiler()); hub.profilerFactory = factory; var tr = hub.startTransactionWithContext(fixture._context); @@ -397,7 +397,7 @@ void main() { hub.options.profilesSampleRate = 1.0; tr = hub.startTransactionWithContext(fixture._context); expect((tr as SentryTracer).profiler, isNotNull); - verify(factory.startProfiling(fixture._context)).called(1); + verify(factory.startProfiler(fixture._context)).called(1); }); test('profiler.finish() is called', () async { @@ -405,7 +405,7 @@ void main() { final factory = MockProfilerFactory(); final profiler = MockProfiler(); final expected = MockProfileInfo(); - when(factory.startProfiling(fixture._context)).thenReturn(profiler); + when(factory.startProfiler(fixture._context)).thenReturn(profiler); when(profiler.finishFor(any)).thenAnswer((_) async => expected); hub.profilerFactory = factory; @@ -421,7 +421,7 @@ void main() { final factory = MockProfilerFactory(); final profiler = MockProfiler(); final expected = MockProfileInfo(); - when(factory.startProfiling(fixture._context)).thenReturn(profiler); + when(factory.startProfiler(fixture._context)).thenReturn(profiler); when(profiler.finishFor(any)).thenAnswer((_) async => expected); hub.profilerFactory = factory; diff --git a/dart/test/mocks.mocks.dart b/dart/test/mocks.mocks.dart index 249f7e4862..44a1236747 100644 --- a/dart/test/mocks.mocks.dart +++ b/dart/test/mocks.mocks.dart @@ -40,9 +40,9 @@ class MockProfilerFactory extends _i1.Mock implements _i3.ProfilerFactory { } @override - _i3.Profiler? startProfiling(_i2.SentryTransactionContext? context) => + _i3.Profiler? startProfiler(_i2.SentryTransactionContext? context) => (super.noSuchMethod(Invocation.method( - #startProfiling, + #startProfiler, [context], )) as _i3.Profiler?); } diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index 6308ae4b4a..02f2ab57f3 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -153,8 +153,11 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { removeTag(key: key, result: result) #if !os(tvOS) && !os(watchOS) - case "startProfiling": - startProfiling(call, result) + case "startProfiler": + startProfiler(call, result) + + case "discardProfiler": + discardProfiler(call, result) case "collectProfile": collectProfile(call, result) @@ -559,14 +562,14 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { } } - private func startProfiling(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + private func startProfiler(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { guard let traceId = call.arguments as? String else { print("Cannot start profiling: trace ID missing") result(FlutterError(code: "5", message: "Cannot start profiling: trace ID missing", details: nil)) return } - let startTime = PrivateSentrySDKOnly.startProfiling(forTrace: SentryId(uuidString: traceId)) + let startTime = PrivateSentrySDKOnly.startProfiler(forTrace: SentryId(uuidString: traceId)) result(startTime) } @@ -584,9 +587,26 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { return } - let payload = PrivateSentrySDKOnly.collectProfile(forTrace: SentryId(uuidString: traceId), since: startTime) + guard let endTime = arguments["endTime"] as? UInt64 else { + print("Cannot collect profile: end time missing") + result(FlutterError(code: "8", message: "Cannot collect profile: end time missing", details: nil)) + return + } + + let payload = PrivateSentrySDKOnly.collectProfileBetween(startTime, and: endTime, forTrace: SentryId(uuidString: traceId)) result(payload) } + + private func discardProfiler(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + guard let traceId = call.arguments as? String else { + print("Cannot discard a profiler: trace ID missing") + result(FlutterError(code: "9", message: "Cannot discard a profiler: trace ID missing", details: nil)) + return + } + + PrivateSentrySDKOnly.discardProfiler(forTrace: SentryId(uuidString: traceId)) + result(nil) + } } // swiftlint:enable function_body_length diff --git a/flutter/lib/src/profiling.dart b/flutter/lib/src/profiling.dart index 6adf12deac..d0b5bd6dee 100644 --- a/flutter/lib/src/profiling.dart +++ b/flutter/lib/src/profiling.dart @@ -13,8 +13,9 @@ import 'sentry_native.dart'; // ignore: invalid_use_of_internal_member class NativeProfilerFactory implements ProfilerFactory { final SentryNative _native; + final ClockProvider _clock; - NativeProfilerFactory(this._native); + NativeProfilerFactory(this._native, this._clock); static void attachTo(Hub hub) { // ignore: invalid_use_of_internal_member @@ -32,24 +33,26 @@ class NativeProfilerFactory implements ProfilerFactory { if (options.platformChecker.platform.isMacOS || options.platformChecker.platform.isIOS) { // ignore: invalid_use_of_internal_member - hub.profilerFactory = NativeProfilerFactory(SentryNative()); + hub.profilerFactory = + // ignore: invalid_use_of_internal_member + NativeProfilerFactory(SentryNative(), options.clock); } } @override - NativeProfiler? startProfiling(SentryTransactionContext context) { + NativeProfiler? startProfiler(SentryTransactionContext context) { if (context.traceId == SentryId.empty()) { return null; } - final startTime = _native.startProfiling(context.traceId); + final startTime = _native.startProfiler(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); + return NativeProfiler(_native, startTime, context.traceId, _clock); } } @@ -60,33 +63,45 @@ class NativeProfiler implements Profiler { final SentryNative _native; final Future _startTime; final SentryId _traceId; + bool _finished = false; + final ClockProvider _clock; - NativeProfiler(this._native, this._startTime, this._traceId); + NativeProfiler(this._native, this._startTime, this._traceId, this._clock); @override void dispose() { - // TODO expose in the cocoa SDK - // _startTime.then((_) => _native.discardProfiling(this._traceId)); + if (!_finished) { + _finished = true; + _startTime.then((_) => _native.discardProfiler(_traceId)); + } } @override Future finishFor(SentryTransaction transaction) async { - final starTime = await _startTime; - if (starTime == null) { + if (_finished) { + return null; + } + _finished = true; + + final starTimeNs = await _startTime; + if (starTimeNs == null) { return null; } - final payload = await _native.collectProfile(_traceId, starTime); + // ignore: invalid_use_of_internal_member + final transactionEndTime = transaction.timestamp ?? _clock(); + final duration = transactionEndTime.difference(transaction.startTimestamp); + final endTimeNs = starTimeNs + (duration.inMicroseconds * 1000); + + final payload = + await _native.collectProfile(_traceId, starTimeNs, endTimeNs); 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["transaction"]["id"] = transaction.eventId.toString(); + payload["transaction"]["trace_id"] = _traceId.toString(); + payload["transaction"]["name"] = transaction.transaction; payload["timestamp"] = transaction.startTimestamp.toIso8601String(); return NativeProfileInfo(payload); } diff --git a/flutter/lib/src/sentry_native.dart b/flutter/lib/src/sentry_native.dart index 67cd7ac067..376d403133 100644 --- a/flutter/lib/src/sentry_native.dart +++ b/flutter/lib/src/sentry_native.dart @@ -92,13 +92,17 @@ class SentryNative { return await _nativeChannel?.removeTag(key); } - Future startProfiling(SentryId traceId) async { - return _nativeChannel?.startProfiling(traceId); + Future startProfiler(SentryId traceId) async { + return _nativeChannel?.startProfiler(traceId); + } + + Future discardProfiler(SentryId traceId) async { + return _nativeChannel?.discardProfiler(traceId); } Future?> collectProfile( - SentryId traceId, int startTimeNs) async { - return _nativeChannel?.collectProfile(traceId, startTimeNs); + SentryId traceId, int startTimeNs, int endTimeNs) async { + return _nativeChannel?.collectProfile(traceId, startTimeNs, endTimeNs); } /// Reset state diff --git a/flutter/lib/src/sentry_native_channel.dart b/flutter/lib/src/sentry_native_channel.dart index bc22b5a1d4..e0d70413f3 100644 --- a/flutter/lib/src/sentry_native_channel.dart +++ b/flutter/lib/src/sentry_native_channel.dart @@ -138,21 +138,32 @@ class SentryNativeChannel { } } - Future startProfiling(SentryId traceId) async { + Future startProfiler(SentryId traceId) async { try { - return await _channel.invokeMethod('startProfiling', traceId.toString()) + return await _channel.invokeMethod('startProfiler', traceId.toString()) as int?; } catch (error, stackTrace) { - _logError('startProfiling', error, stackTrace); + _logError('startProfiler', error, stackTrace); return null; } } + Future discardProfiler(SentryId traceId) async { + try { + return await _channel.invokeMethod('discardProfiler', traceId.toString()); + } catch (error, stackTrace) { + _logError('discardProfiler', error, stackTrace); + } + } + Future?> collectProfile( - SentryId traceId, int startTimeNs) async { + SentryId traceId, int startTimeNs, int endTimeNs) async { try { - return await _channel.invokeMapMethod('collectProfile', - {'traceId': traceId.toString(), 'startTime': startTimeNs}); + return await _channel.invokeMapMethod('collectProfile', { + 'traceId': traceId.toString(), + 'startTime': startTimeNs, + 'endTime': endTimeNs, + }); } catch (error, stackTrace) { _logError('collectProfile', error, stackTrace); return null; diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index 1b86552923..52bdae7026 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -40,6 +40,7 @@ ISentrySpan startTransactionShim( Transport, // ignore: invalid_use_of_internal_member SentryTracer, + SentryTransaction, MethodChannel, ], customMocks: [ MockSpec(fallbackGenerators: {#startTransaction: startTransactionShim}) @@ -189,7 +190,8 @@ class TestMockSentryNative implements SentryNative { var numberOfSetTagCalls = 0; SentryUser? sentryUser; var numberOfSetUserCalls = 0; - var numberOfStartProfilingCalls = 0; + var numberOfStartProfilerCalls = 0; + var numberOfDiscardProfilerCalls = 0; var numberOfCollectProfileCalls = 0; @override @@ -270,14 +272,20 @@ class TestMockSentryNative implements SentryNative { @override Future?> collectProfile( - SentryId traceId, int startTimeNs) { + SentryId traceId, int startTimeNs, int endTimeNs) { numberOfCollectProfileCalls++; return Future.value(null); } @override - Future startProfiling(SentryId traceId) { - numberOfStartProfilingCalls++; + Future startProfiler(SentryId traceId) { + numberOfStartProfilerCalls++; + return Future.value(42); + } + + @override + Future discardProfiler(SentryId traceId) { + numberOfDiscardProfilerCalls++; return Future.value(null); } } @@ -299,7 +307,8 @@ class MockNativeChannel implements SentryNativeChannel { int numberOfSetContextsCalls = 0; int numberOfSetExtraCalls = 0; int numberOfSetTagCalls = 0; - int numberOfStartProfilingCalls = 0; + int numberOfStartProfilerCalls = 0; + int numberOfDiscardProfilerCalls = 0; int numberOfCollectProfileCalls = 0; @override @@ -364,14 +373,20 @@ class MockNativeChannel implements SentryNativeChannel { @override Future?> collectProfile( - SentryId traceId, int startTimeNs) { + SentryId traceId, int startTimeNs, int endTimeNs) { numberOfCollectProfileCalls++; return Future.value(null); } @override - Future startProfiling(SentryId traceId) { - numberOfStartProfilingCalls++; + Future startProfiler(SentryId traceId) { + numberOfStartProfilerCalls++; + return Future.value(null); + } + + @override + Future discardProfiler(SentryId traceId) { + numberOfDiscardProfilerCalls++; return Future.value(null); } } diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index bb6f7964c9..00f264f9fe 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -3,17 +3,17 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; +import 'dart:async' as _i7; -import 'package:flutter/src/services/binary_messenger.dart' as _i5; -import 'package:flutter/src/services/message_codec.dart' as _i4; +import 'package:flutter/src/services/binary_messenger.dart' as _i6; +import 'package:flutter/src/services/message_codec.dart' as _i5; import 'package:flutter/src/services/platform_channel.dart' as _i10; import 'package:mockito/mockito.dart' as _i1; import 'package:sentry/sentry.dart' as _i2; import 'package:sentry/src/profiling.dart' as _i9; import 'package:sentry/src/protocol.dart' as _i3; -import 'package:sentry/src/sentry_envelope.dart' as _i7; -import 'package:sentry/src/sentry_tracer.dart' as _i8; +import 'package:sentry/src/sentry_envelope.dart' as _i8; +import 'package:sentry/src/sentry_tracer.dart' as _i4; import 'mocks.dart' as _i11; @@ -70,8 +70,8 @@ class _FakeSentryTraceHeader_3 extends _i1.SmartFake ); } -class _FakeMethodCodec_4 extends _i1.SmartFake implements _i4.MethodCodec { - _FakeMethodCodec_4( +class _FakeSentryTracer_4 extends _i1.SmartFake implements _i4.SentryTracer { + _FakeSentryTracer_4( Object parent, Invocation parentInvocation, ) : super( @@ -80,9 +80,8 @@ class _FakeMethodCodec_4 extends _i1.SmartFake implements _i4.MethodCodec { ); } -class _FakeBinaryMessenger_5 extends _i1.SmartFake - implements _i5.BinaryMessenger { - _FakeBinaryMessenger_5( +class _FakeSentryId_5 extends _i1.SmartFake implements _i3.SentryId { + _FakeSentryId_5( Object parent, Invocation parentInvocation, ) : super( @@ -91,8 +90,8 @@ class _FakeBinaryMessenger_5 extends _i1.SmartFake ); } -class _FakeSentryOptions_6 extends _i1.SmartFake implements _i2.SentryOptions { - _FakeSentryOptions_6( +class _FakeContexts_6 extends _i1.SmartFake implements _i3.Contexts { + _FakeContexts_6( Object parent, Invocation parentInvocation, ) : super( @@ -101,8 +100,9 @@ class _FakeSentryOptions_6 extends _i1.SmartFake implements _i2.SentryOptions { ); } -class _FakeSentryId_7 extends _i1.SmartFake implements _i3.SentryId { - _FakeSentryId_7( +class _FakeSentryTransaction_7 extends _i1.SmartFake + implements _i3.SentryTransaction { + _FakeSentryTransaction_7( Object parent, Invocation parentInvocation, ) : super( @@ -111,8 +111,39 @@ class _FakeSentryId_7 extends _i1.SmartFake implements _i3.SentryId { ); } -class _FakeHub_8 extends _i1.SmartFake implements _i2.Hub { - _FakeHub_8( +class _FakeMethodCodec_8 extends _i1.SmartFake implements _i5.MethodCodec { + _FakeMethodCodec_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeBinaryMessenger_9 extends _i1.SmartFake + implements _i6.BinaryMessenger { + _FakeBinaryMessenger_9( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeSentryOptions_10 extends _i1.SmartFake implements _i2.SentryOptions { + _FakeSentryOptions_10( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeHub_11 extends _i1.SmartFake implements _i2.Hub { + _FakeHub_11( Object parent, Invocation parentInvocation, ) : super( @@ -130,20 +161,20 @@ class MockTransport extends _i1.Mock implements _i2.Transport { } @override - _i6.Future<_i3.SentryId?> send(_i7.SentryEnvelope? envelope) => + _i7.Future<_i3.SentryId?> send(_i8.SentryEnvelope? envelope) => (super.noSuchMethod( Invocation.method( #send, [envelope], ), - returnValue: _i6.Future<_i3.SentryId?>.value(), - ) as _i6.Future<_i3.SentryId?>); + returnValue: _i7.Future<_i3.SentryId?>.value(), + ) as _i7.Future<_i3.SentryId?>); } /// A class which mocks [SentryTracer]. /// /// See the documentation for Mockito's code generation for more information. -class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { +class MockSentryTracer extends _i1.Mock implements _i4.SentryTracer { MockSentryTracer() { _i1.throwOnMissingStub(this); } @@ -259,7 +290,7 @@ class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { returnValue: {}, ) as Map); @override - _i6.Future finish({ + _i7.Future finish({ _i3.SpanStatus? status, DateTime? endTimestamp, }) => @@ -272,9 +303,9 @@ class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { #endTimestamp: endTimestamp, }, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void removeData(String? key) => super.noSuchMethod( Invocation.method( @@ -423,6 +454,215 @@ class MockSentryTracer extends _i1.Mock implements _i8.SentryTracer { ); } +/// A class which mocks [SentryTransaction]. +/// +/// See the documentation for Mockito's code generation for more information. +// ignore: must_be_immutable +class MockSentryTransaction extends _i1.Mock implements _i3.SentryTransaction { + MockSentryTransaction() { + _i1.throwOnMissingStub(this); + } + + @override + DateTime get startTimestamp => (super.noSuchMethod( + Invocation.getter(#startTimestamp), + returnValue: _FakeDateTime_1( + this, + Invocation.getter(#startTimestamp), + ), + ) as DateTime); + @override + set startTimestamp(DateTime? _startTimestamp) => super.noSuchMethod( + Invocation.setter( + #startTimestamp, + _startTimestamp, + ), + returnValueForMissingStub: null, + ); + @override + List<_i3.SentrySpan> get spans => (super.noSuchMethod( + Invocation.getter(#spans), + returnValue: <_i3.SentrySpan>[], + ) as List<_i3.SentrySpan>); + @override + set spans(List<_i3.SentrySpan>? _spans) => super.noSuchMethod( + Invocation.setter( + #spans, + _spans, + ), + returnValueForMissingStub: null, + ); + @override + _i4.SentryTracer get tracer => (super.noSuchMethod( + Invocation.getter(#tracer), + returnValue: _FakeSentryTracer_4( + this, + Invocation.getter(#tracer), + ), + ) as _i4.SentryTracer); + @override + Map get measurements => (super.noSuchMethod( + Invocation.getter(#measurements), + returnValue: {}, + ) as Map); + @override + set measurements(Map? _measurements) => + super.noSuchMethod( + Invocation.setter( + #measurements, + _measurements, + ), + returnValueForMissingStub: null, + ); + @override + set transactionInfo(_i3.SentryTransactionInfo? _transactionInfo) => + super.noSuchMethod( + Invocation.setter( + #transactionInfo, + _transactionInfo, + ), + returnValueForMissingStub: null, + ); + @override + bool get finished => (super.noSuchMethod( + Invocation.getter(#finished), + returnValue: false, + ) as bool); + @override + bool get sampled => (super.noSuchMethod( + Invocation.getter(#sampled), + returnValue: false, + ) as bool); + @override + _i3.SentryId get eventId => (super.noSuchMethod( + Invocation.getter(#eventId), + returnValue: _FakeSentryId_5( + this, + Invocation.getter(#eventId), + ), + ) as _i3.SentryId); + @override + _i3.Contexts get contexts => (super.noSuchMethod( + Invocation.getter(#contexts), + returnValue: _FakeContexts_6( + this, + Invocation.getter(#contexts), + ), + ) as _i3.Contexts); + @override + Map toJson() => (super.noSuchMethod( + Invocation.method( + #toJson, + [], + ), + returnValue: {}, + ) as Map); + @override + _i3.SentryTransaction copyWith({ + _i3.SentryId? eventId, + DateTime? timestamp, + String? platform, + String? logger, + String? serverName, + String? release, + String? dist, + String? environment, + Map? modules, + _i3.SentryMessage? message, + String? transaction, + dynamic throwable, + _i3.SentryLevel? level, + String? culprit, + Map? tags, + Map? extra, + List? fingerprint, + _i3.SentryUser? user, + _i3.Contexts? contexts, + List<_i3.Breadcrumb>? breadcrumbs, + _i3.SdkVersion? sdk, + _i3.SentryRequest? request, + _i3.DebugMeta? debugMeta, + List<_i3.SentryException>? exceptions, + List<_i3.SentryThread>? threads, + String? type, + Map? measurements, + _i3.SentryTransactionInfo? transactionInfo, + }) => + (super.noSuchMethod( + Invocation.method( + #copyWith, + [], + { + #eventId: eventId, + #timestamp: timestamp, + #platform: platform, + #logger: logger, + #serverName: serverName, + #release: release, + #dist: dist, + #environment: environment, + #modules: modules, + #message: message, + #transaction: transaction, + #throwable: throwable, + #level: level, + #culprit: culprit, + #tags: tags, + #extra: extra, + #fingerprint: fingerprint, + #user: user, + #contexts: contexts, + #breadcrumbs: breadcrumbs, + #sdk: sdk, + #request: request, + #debugMeta: debugMeta, + #exceptions: exceptions, + #threads: threads, + #type: type, + #measurements: measurements, + #transactionInfo: transactionInfo, + }, + ), + returnValue: _FakeSentryTransaction_7( + this, + Invocation.method( + #copyWith, + [], + { + #eventId: eventId, + #timestamp: timestamp, + #platform: platform, + #logger: logger, + #serverName: serverName, + #release: release, + #dist: dist, + #environment: environment, + #modules: modules, + #message: message, + #transaction: transaction, + #throwable: throwable, + #level: level, + #culprit: culprit, + #tags: tags, + #extra: extra, + #fingerprint: fingerprint, + #user: user, + #contexts: contexts, + #breadcrumbs: breadcrumbs, + #sdk: sdk, + #request: request, + #debugMeta: debugMeta, + #exceptions: exceptions, + #threads: threads, + #type: type, + #measurements: measurements, + #transactionInfo: transactionInfo, + }, + ), + ), + ) as _i3.SentryTransaction); +} + /// A class which mocks [MethodChannel]. /// /// See the documentation for Mockito's code generation for more information. @@ -437,23 +677,23 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { returnValue: '', ) as String); @override - _i4.MethodCodec get codec => (super.noSuchMethod( + _i5.MethodCodec get codec => (super.noSuchMethod( Invocation.getter(#codec), - returnValue: _FakeMethodCodec_4( + returnValue: _FakeMethodCodec_8( this, Invocation.getter(#codec), ), - ) as _i4.MethodCodec); + ) as _i5.MethodCodec); @override - _i5.BinaryMessenger get binaryMessenger => (super.noSuchMethod( + _i6.BinaryMessenger get binaryMessenger => (super.noSuchMethod( Invocation.getter(#binaryMessenger), - returnValue: _FakeBinaryMessenger_5( + returnValue: _FakeBinaryMessenger_9( this, Invocation.getter(#binaryMessenger), ), - ) as _i5.BinaryMessenger); + ) as _i6.BinaryMessenger); @override - _i6.Future invokeMethod( + _i7.Future invokeMethod( String? method, [ dynamic arguments, ]) => @@ -465,10 +705,10 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { arguments, ], ), - returnValue: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i7.Future.value(), + ) as _i7.Future); @override - _i6.Future?> invokeListMethod( + _i7.Future?> invokeListMethod( String? method, [ dynamic arguments, ]) => @@ -480,10 +720,10 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { arguments, ], ), - returnValue: _i6.Future?>.value(), - ) as _i6.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override - _i6.Future?> invokeMapMethod( + _i7.Future?> invokeMapMethod( String? method, [ dynamic arguments, ]) => @@ -495,11 +735,11 @@ class MockMethodChannel extends _i1.Mock implements _i10.MethodChannel { arguments, ], ), - returnValue: _i6.Future?>.value(), - ) as _i6.Future?>); + returnValue: _i7.Future?>.value(), + ) as _i7.Future?>); @override void setMethodCallHandler( - _i6.Future Function(_i4.MethodCall)? handler) => + _i7.Future Function(_i5.MethodCall)? handler) => super.noSuchMethod( Invocation.method( #setMethodCallHandler, @@ -520,7 +760,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i2.SentryOptions get options => (super.noSuchMethod( Invocation.getter(#options), - returnValue: _FakeSentryOptions_6( + returnValue: _FakeSentryOptions_10( this, Invocation.getter(#options), ), @@ -533,7 +773,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { @override _i3.SentryId get lastEventId => (super.noSuchMethod( Invocation.getter(#lastEventId), - returnValue: _FakeSentryId_7( + returnValue: _FakeSentryId_5( this, Invocation.getter(#lastEventId), ), @@ -547,7 +787,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { returnValueForMissingStub: null, ); @override - _i6.Future<_i3.SentryId> captureEvent( + _i7.Future<_i3.SentryId> captureEvent( _i3.SentryEvent? event, { dynamic stackTrace, _i2.Hint? hint, @@ -563,7 +803,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i6.Future<_i3.SentryId>.value(_FakeSentryId_7( + returnValue: _i7.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureEvent, @@ -575,9 +815,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i6.Future<_i3.SentryId>); + ) as _i7.Future<_i3.SentryId>); @override - _i6.Future<_i3.SentryId> captureException( + _i7.Future<_i3.SentryId> captureException( dynamic throwable, { dynamic stackTrace, _i2.Hint? hint, @@ -593,7 +833,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i6.Future<_i3.SentryId>.value(_FakeSentryId_7( + returnValue: _i7.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureException, @@ -605,9 +845,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i6.Future<_i3.SentryId>); + ) as _i7.Future<_i3.SentryId>); @override - _i6.Future<_i3.SentryId> captureMessage( + _i7.Future<_i3.SentryId> captureMessage( String? message, { _i3.SentryLevel? level, String? template, @@ -627,7 +867,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #withScope: withScope, }, ), - returnValue: _i6.Future<_i3.SentryId>.value(_FakeSentryId_7( + returnValue: _i7.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureMessage, @@ -641,19 +881,19 @@ class MockHub extends _i1.Mock implements _i2.Hub { }, ), )), - ) as _i6.Future<_i3.SentryId>); + ) as _i7.Future<_i3.SentryId>); @override - _i6.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => + _i7.Future captureUserFeedback(_i2.SentryUserFeedback? userFeedback) => (super.noSuchMethod( Invocation.method( #captureUserFeedback, [userFeedback], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i6.Future addBreadcrumb( + _i7.Future addBreadcrumb( _i3.Breadcrumb? crumb, { _i2.Hint? hint, }) => @@ -663,9 +903,9 @@ class MockHub extends _i1.Mock implements _i2.Hub { [crumb], {#hint: hint}, ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override void bindClient(_i2.SentryClient? client) => super.noSuchMethod( Invocation.method( @@ -680,7 +920,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { #clone, [], ), - returnValue: _FakeHub_8( + returnValue: _FakeHub_11( this, Invocation.method( #clone, @@ -689,20 +929,20 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ) as _i2.Hub); @override - _i6.Future close() => (super.noSuchMethod( + _i7.Future close() => (super.noSuchMethod( Invocation.method( #close, [], ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); + returnValue: _i7.Future.value(), + returnValueForMissingStub: _i7.Future.value(), + ) as _i7.Future); @override - _i6.FutureOr configureScope(_i2.ScopeCallback? callback) => + _i7.FutureOr configureScope(_i2.ScopeCallback? callback) => (super.noSuchMethod(Invocation.method( #configureScope, [callback], - )) as _i6.FutureOr); + )) as _i7.FutureOr); @override _i2.ISentrySpan startTransaction( String? name, @@ -790,7 +1030,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { ), ) as _i2.ISentrySpan); @override - _i6.Future<_i3.SentryId> captureTransaction( + _i7.Future<_i3.SentryId> captureTransaction( _i3.SentryTransaction? transaction, { _i2.SentryTraceContextHeader? traceContext, }) => @@ -800,7 +1040,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { [transaction], {#traceContext: traceContext}, ), - returnValue: _i6.Future<_i3.SentryId>.value(_FakeSentryId_7( + returnValue: _i7.Future<_i3.SentryId>.value(_FakeSentryId_5( this, Invocation.method( #captureTransaction, @@ -808,7 +1048,7 @@ class MockHub extends _i1.Mock implements _i2.Hub { {#traceContext: traceContext}, ), )), - ) as _i6.Future<_i3.SentryId>); + ) as _i7.Future<_i3.SentryId>); @override void setSpanContext( dynamic throwable, diff --git a/flutter/test/profiling_test.dart b/flutter/test/profiling_test.dart index 605ca6c1a2..7f4eb92263 100644 --- a/flutter/test/profiling_test.dart +++ b/flutter/test/profiling_test.dart @@ -34,13 +34,62 @@ void main() { test('creates a profiler', () async { final nativeMock = TestMockSentryNative(); - final sut = NativeProfilerFactory(nativeMock); - final profiler = sut.startProfiling(SentryTransactionContext( + // ignore: invalid_use_of_internal_member + final sut = NativeProfilerFactory(nativeMock, getUtcDateTime); + final profiler = sut.startProfiler(SentryTransactionContext( + 'name', + 'op', + )); + expect(nativeMock.numberOfStartProfilerCalls, 1); + expect(profiler, isNotNull); + }); + }); + + group('$NativeProfiler', () { + late TestMockSentryNative nativeMock; + late NativeProfiler sut; + + setUp(() { + nativeMock = TestMockSentryNative(); + // ignore: invalid_use_of_internal_member + final factory = NativeProfilerFactory(nativeMock, getUtcDateTime); + final profiler = factory.startProfiler(SentryTransactionContext( 'name', 'op', )); - expect(nativeMock.numberOfStartProfilingCalls, 1); + expect(nativeMock.numberOfStartProfilerCalls, 1); expect(profiler, isNotNull); + sut = profiler!; + }); + + test('dispose() calls native discard() exactly once', () async { + sut.dispose(); + sut.dispose(); // Additional calls must not have an effect. + + // Yield to let the .then() in .dispose() execute. + await null; + await null; + + expect(nativeMock.numberOfDiscardProfilerCalls, 1); + + // finishFor() mustn't work after disposing + expect(await sut.finishFor(MockSentryTransaction()), isNull); + expect(nativeMock.numberOfCollectProfileCalls, 0); + }); + + test('dispose() does not call discard() after finishing', () async { + final mockTransaction = MockSentryTransaction(); + when(mockTransaction.startTimestamp).thenReturn(DateTime.now()); + when(mockTransaction.timestamp).thenReturn(DateTime.now()); + expect(await sut.finishFor(mockTransaction), isNull); + + sut.dispose(); + + // Yield to let the .then() in .dispose() execute. + await null; + + expect(nativeMock.numberOfDiscardProfilerCalls, 0); + expect(nativeMock.numberOfCollectProfileCalls, 1); }); }); } diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index d71c6bdbc8..c90c9d9106 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -187,32 +187,50 @@ void main() { .invokeMethod('removeTag', {'key': 'fixture-key'})); }); - test('startProfiling', () async { + test('startProfiler', () async { final traceId = SentryId.newId(); when(fixture.methodChannel - .invokeMethod('startProfiling', traceId.toString())) - .thenAnswer((_) => Future.value()); + .invokeMethod('startProfiler', traceId.toString())) + .thenAnswer((_) async {}); + + final sut = fixture.getSut(); + await sut.startProfiler(traceId); + + verify(fixture.methodChannel + .invokeMethod('startProfiler', traceId.toString())); + }); + + test('discardProfiler', () async { + final traceId = SentryId.newId(); + when(fixture.methodChannel + .invokeMethod('discardProfiler', traceId.toString())) + .thenAnswer((_) async {}); final sut = fixture.getSut(); - await sut.startProfiling(traceId); + await sut.discardProfiler(traceId); verify(fixture.methodChannel - .invokeMethod('startProfiling', traceId.toString())); + .invokeMethod('discardProfiler', traceId.toString())); }); test('collectProfile', () async { final traceId = SentryId.newId(); const startTime = 42; + const endTime = 50; when(fixture.methodChannel.invokeMapMethod('collectProfile', { 'traceId': traceId.toString(), - 'startTime': startTime + 'startTime': startTime, + 'endTime': endTime, })).thenAnswer((_) => Future.value()); final sut = fixture.getSut(); - await sut.collectProfile(traceId, startTime); + await sut.collectProfile(traceId, startTime, endTime); - verify(fixture.methodChannel.invokeMapMethod('collectProfile', - {'traceId': traceId.toString(), 'startTime': startTime})); + verify(fixture.methodChannel.invokeMapMethod('collectProfile', { + 'traceId': traceId.toString(), + 'startTime': startTime, + 'endTime': endTime, + })); }); }); } diff --git a/flutter/test/sentry_native_test.dart b/flutter/test/sentry_native_test.dart index d55bc5337c..cf0a93a7ae 100644 --- a/flutter/test/sentry_native_test.dart +++ b/flutter/test/sentry_native_test.dart @@ -117,16 +117,23 @@ void main() { expect(fixture.channel.numberOfRemoveTagCalls, 1); }); - test('startProfiling', () async { + test('startProfiler', () async { final sut = fixture.getSut(); - await sut.startProfiling(SentryId.newId()); + await sut.startProfiler(SentryId.newId()); - expect(fixture.channel.numberOfStartProfilingCalls, 1); + expect(fixture.channel.numberOfStartProfilerCalls, 1); + }); + + test('discardProfiler', () async { + final sut = fixture.getSut(); + await sut.discardProfiler(SentryId.newId()); + + expect(fixture.channel.numberOfDiscardProfilerCalls, 1); }); test('collectProfile', () async { final sut = fixture.getSut(); - await sut.collectProfile(SentryId.newId(), 42); + await sut.collectProfile(SentryId.newId(), 1, 2); expect(fixture.channel.numberOfCollectProfileCalls, 1); });