Skip to content

Commit 7b392e4

Browse files
committed
feat: prepare profiler interfaces and hub integration
1 parent f9fd5d8 commit 7b392e4

File tree

12 files changed

+305
-7
lines changed

12 files changed

+305
-7
lines changed

dart/lib/src/hub.dart

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:collection';
33

44
import 'package:meta/meta.dart';
5+
import 'profiling.dart';
56
import 'transport/data_category.dart';
67

78
import '../sentry.dart';
@@ -433,12 +434,12 @@ class Hub {
433434
} else {
434435
final item = _peek();
435436

436-
final samplingContext = SentrySamplingContext(
437-
transactionContext, customSamplingContext ?? {});
438-
439437
// if transactionContext has no sampled decision, run the traces sampler
440-
if (transactionContext.samplingDecision == null) {
441-
final samplingDecision = _tracesSampler.sample(samplingContext);
438+
var samplingDecision = transactionContext.samplingDecision;
439+
if (samplingDecision == null) {
440+
final samplingContext = SentrySamplingContext(
441+
transactionContext, customSamplingContext ?? {});
442+
samplingDecision = _tracesSampler.sample(samplingContext);
442443
transactionContext =
443444
transactionContext.copyWith(samplingDecision: samplingDecision);
444445
}
@@ -449,6 +450,12 @@ class Hub {
449450
);
450451
}
451452

453+
Profiler? profiler;
454+
if (_profilerFactory != null &&
455+
_tracesSampler.sampleProfiling(samplingDecision)) {
456+
profiler = _profilerFactory?.startProfiling(transactionContext);
457+
}
458+
452459
final tracer = SentryTracer(
453460
transactionContext,
454461
this,
@@ -457,6 +464,7 @@ class Hub {
457464
autoFinishAfter: autoFinishAfter,
458465
trimEnd: trimEnd ?? false,
459466
onFinish: onFinish,
467+
profiler: profiler,
460468
);
461469
if (bindToScope ?? false) {
462470
item.scope.span = tracer;
@@ -552,6 +560,11 @@ class Hub {
552560
) =>
553561
_throwableToSpan.add(throwable, span, transaction);
554562

563+
@internal
564+
set profilerFactory(ProfilerFactory? value) => _profilerFactory = value;
565+
566+
ProfilerFactory? _profilerFactory;
567+
555568
SentryEvent _assignTraceContext(SentryEvent event) {
556569
// assign trace context
557570
if (event.throwable != null && event.contexts.trace == null) {

dart/lib/src/hub_adapter.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
44
import 'hint.dart';
55

66
import 'hub.dart';
7+
import 'profiling.dart';
78
import 'protocol.dart';
89
import 'sentry.dart';
910
import 'sentry_client.dart';
@@ -166,4 +167,9 @@ class HubAdapter implements Hub {
166167
String transaction,
167168
) =>
168169
Sentry.currentHub.setSpanContext(throwable, span, transaction);
170+
171+
@internal
172+
@override
173+
set profilerFactory(ProfilerFactory? value) =>
174+
Sentry.currentHub.profilerFactory = value;
169175
}

dart/lib/src/noop_hub.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
44

55
import 'hint.dart';
66
import 'hub.dart';
7+
import 'profiling.dart';
78
import 'protocol.dart';
89
import 'sentry_client.dart';
910
import 'sentry_options.dart';
@@ -118,4 +119,8 @@ class NoOpHub implements Hub {
118119

119120
@override
120121
void setSpanContext(throwable, ISentrySpan span, String transaction) {}
122+
123+
@internal
124+
@override
125+
set profilerFactory(ProfilerFactory? value) {}
121126
}

dart/lib/src/profiling.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'dart:async';
2+
3+
import 'package:meta/meta.dart';
4+
5+
import '../sentry.dart';
6+
7+
@internal
8+
abstract class ProfilerFactory {
9+
Profiler startProfiling(SentryTransactionContext context);
10+
}
11+
12+
@internal
13+
abstract class Profiler {
14+
Future<ProfileInfo> finishFor(SentryTransaction transaction);
15+
void dispose();
16+
}
17+
18+
// See https://develop.sentry.dev/sdk/profiles/
19+
@internal
20+
abstract class ProfileInfo {
21+
FutureOr<SentryEnvelopeItem> asEnvelopeItem();
22+
}

dart/lib/src/protocol/sentry_transaction.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:meta/meta.dart';
22

3+
import '../profiling.dart';
34
import '../protocol.dart';
45
import '../sentry_tracer.dart';
56
import '../utils.dart';
@@ -14,6 +15,9 @@ class SentryTransaction extends SentryEvent {
1415
late final Map<String, SentryMeasurement> measurements;
1516
late final SentryTransactionInfo? transactionInfo;
1617

18+
@internal
19+
late final ProfileInfo? profileInfo;
20+
1721
SentryTransaction(
1822
this._tracer, {
1923
SentryId? eventId,

dart/lib/src/sentry_options.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ class SentryOptions {
289289
/// to be sent to Sentry.
290290
TracesSamplerCallback? tracesSampler;
291291

292+
double? _profilesSampleRate;
293+
294+
/// The sample rate for profiling traces in the range [0.0, 1.0].
295+
/// This is relative to tracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
296+
/// At the moment, only apps targeting iOS and macOS are supported.
297+
@experimental
298+
double? get profilesSampleRate => _profilesSampleRate;
299+
300+
/// The sample rate for profiling traces in the range [0.0, 1.0].
301+
/// This is relative to tracesSampleRate - it is a ratio of profiled traces out of all sampled traces.
302+
/// At the moment, only apps targeting iOS and macOS are supported.
303+
@experimental
304+
set profilesSampleRate(double? value) {
305+
assert(value == null || (value >= 0 && value <= 1));
306+
_profilesSampleRate = value;
307+
}
308+
292309
/// Send statistics to sentry when the client drops events.
293310
bool sendClientReports = true;
294311

dart/lib/src/sentry_tracer.dart

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:async';
33
import 'package:meta/meta.dart';
44

55
import '../sentry.dart';
6+
import 'profiling.dart';
67
import 'sentry_tracer_finish_status.dart';
78
import 'utils/sample_rate_format.dart';
89

@@ -31,6 +32,8 @@ class SentryTracer extends ISentrySpan {
3132

3233
SentryTraceContextHeader? _sentryTraceContextHeader;
3334

35+
late final Profiler? profiler;
36+
3437
/// If [waitForChildren] is true, this transaction will not finish until all
3538
/// its children are finished.
3639
///
@@ -52,6 +55,7 @@ class SentryTracer extends ISentrySpan {
5255
Duration? autoFinishAfter,
5356
bool trimEnd = false,
5457
OnTransactionFinish? onFinish,
58+
this.profiler,
5559
}) {
5660
_rootSpan = SentrySpan(
5761
this,
@@ -77,8 +81,13 @@ class SentryTracer extends ISentrySpan {
7781
final commonEndTimestamp = endTimestamp ?? _hub.options.clock();
7882
_autoFinishAfterTimer?.cancel();
7983
_finishStatus = SentryTracerFinishStatus.finishing(status);
80-
if (!_rootSpan.finished &&
81-
(!_waitForChildren || _haveAllChildrenFinished())) {
84+
if (_rootSpan.finished) {
85+
return;
86+
}
87+
if (_waitForChildren && !_haveAllChildrenFinished()) {
88+
return;
89+
}
90+
try {
8291
_rootSpan.status ??= status;
8392

8493
// remove span where its endTimestamp is before startTimestamp
@@ -131,10 +140,19 @@ class SentryTracer extends ISentrySpan {
131140

132141
final transaction = SentryTransaction(this);
133142
transaction.measurements.addAll(_measurements);
143+
144+
if (profiler != null) {
145+
if (status == null || status == SpanStatus.ok()) {
146+
transaction.profileInfo = await profiler?.finishFor(transaction);
147+
}
148+
}
149+
134150
await _hub.captureTransaction(
135151
transaction,
136152
traceContext: traceContext(),
137153
);
154+
} finally {
155+
profiler!.dispose();
138156
}
139157
}
140158

dart/lib/src/sentry_traces_sampler.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,13 @@ class SentryTracesSampler {
6767
return SentryTracesSamplingDecision(false);
6868
}
6969

70+
bool sampleProfiling(SentryTracesSamplingDecision tracesSamplingDecision) {
71+
double? optionsRate = _options.profilesSampleRate;
72+
if (optionsRate == null || !tracesSamplingDecision.sampled) {
73+
return false;
74+
}
75+
return _sample(optionsRate);
76+
}
77+
7078
bool _sample(double result) => !(result < _random.nextDouble());
7179
}

dart/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies:
1818
uuid: ^3.0.0
1919

2020
dev_dependencies:
21+
build_runner: ^2.4.2
2122
mockito: ^5.1.0
2223
lints: ^2.0.0
2324
test: ^1.21.1

dart/test/hub_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import 'package:collection/collection.dart';
2+
import 'package:mockito/mockito.dart';
23
import 'package:sentry/sentry.dart';
34
import 'package:sentry/src/client_reports/discard_reason.dart';
5+
import 'package:sentry/src/profiling.dart';
46
import 'package:sentry/src/sentry_tracer.dart';
57
import 'package:sentry/src/transport/data_category.dart';
68
import 'package:test/test.dart';
79

810
import 'mocks.dart';
11+
import 'mocks.mocks.dart';
912
import 'mocks/mock_client_report_recorder.dart';
1013
import 'mocks/mock_sentry_client.dart';
1114

@@ -374,6 +377,61 @@ void main() {
374377
expect(
375378
fixture.client.captureTransactionCalls.first.traceContext, context);
376379
});
380+
381+
test('profiler is not started by default', () async {
382+
final hub = fixture.getSut();
383+
final tr = hub.startTransaction('name', 'op');
384+
expect(tr, isA<SentryTracer>());
385+
expect((tr as SentryTracer).profiler, isNull);
386+
});
387+
388+
test('profiler is started according to the sampling rate', () async {
389+
final hub = fixture.getSut();
390+
final factory = MockProfilerFactory();
391+
when(factory.startProfiling(fixture._context)).thenReturn(MockProfiler());
392+
hub.profilerFactory = factory;
393+
394+
var tr = hub.startTransactionWithContext(fixture._context);
395+
expect((tr as SentryTracer).profiler, isNull);
396+
verifyZeroInteractions(factory);
397+
398+
hub.options.profilesSampleRate = 1.0;
399+
tr = hub.startTransactionWithContext(fixture._context);
400+
expect((tr as SentryTracer).profiler, isNotNull);
401+
verify(factory.startProfiling(fixture._context)).called(1);
402+
});
403+
404+
test('profiler.finish() is called', () async {
405+
final hub = fixture.getSut();
406+
final factory = MockProfilerFactory();
407+
final profiler = MockProfiler();
408+
final expected = MockProfileInfo();
409+
when(factory.startProfiling(fixture._context)).thenReturn(profiler);
410+
when(profiler.finishFor(any)).thenAnswer((_) async => expected);
411+
412+
hub.profilerFactory = factory;
413+
hub.options.profilesSampleRate = 1.0;
414+
final tr = hub.startTransactionWithContext(fixture._context);
415+
await tr.finish();
416+
verify(profiler.finishFor(any)).called(1);
417+
verify(profiler.dispose()).called(1);
418+
});
419+
420+
test('profiler.dispose() is called even if not captured', () async {
421+
final hub = fixture.getSut();
422+
final factory = MockProfilerFactory();
423+
final profiler = MockProfiler();
424+
final expected = MockProfileInfo();
425+
when(factory.startProfiling(fixture._context)).thenReturn(profiler);
426+
when(profiler.finishFor(any)).thenAnswer((_) async => expected);
427+
428+
hub.profilerFactory = factory;
429+
hub.options.profilesSampleRate = 1.0;
430+
final tr = hub.startTransactionWithContext(fixture._context);
431+
await tr.finish(status: SpanStatus.aborted());
432+
verify(profiler.dispose()).called(1);
433+
verifyNever(profiler.finishFor(any));
434+
});
377435
});
378436

379437
group('Hub scope', () {
@@ -641,10 +699,12 @@ class Fixture {
641699

642700
final hub = Hub(options);
643701

702+
// A fully configured context - won't trigger a copy in startTransaction().
644703
_context = SentryTransactionContext(
645704
'name',
646705
'op',
647706
samplingDecision: SentryTracesSamplingDecision(sampled!),
707+
origin: SentryTraceOrigins.manual,
648708
);
649709

650710
tracer = SentryTracer(_context, hub);

0 commit comments

Comments
 (0)