diff --git a/CHANGELOG.md b/CHANGELOG.md index 41755d841d..33e1d579a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Enhancements +- Propagate sample seed in baggage header ([#2629](https://github.com/getsentry/sentry-dart/pull/2629)) + - Read more about the specs [here](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) - Finish and start new transaction when tapping same element again ([#2623](https://github.com/getsentry/sentry-dart/pull/2623)) ### Fixes diff --git a/dart/lib/src/sentry_baggage.dart b/dart/lib/src/sentry_baggage.dart index a8b38e375f..8def45ca0f 100644 --- a/dart/lib/src/sentry_baggage.dart +++ b/dart/lib/src/sentry_baggage.dart @@ -1,11 +1,12 @@ import 'package:meta/meta.dart'; -import 'scope.dart'; import 'protocol.dart'; - +import 'scope.dart'; import 'sentry_options.dart'; class SentryBaggage { static const String _sampleRateKeyName = 'sentry-sample_rate'; + static const String _sampleRandKeyName = 'sentry-sample_rand'; + static const int _maxChars = 8192; static const int _maxListMember = 64; @@ -194,6 +195,10 @@ class SentryBaggage { set(_sampleRateKeyName, value); } + void setSampleRand(String value) { + set(_sampleRandKeyName, value); + } + void setSampled(String value) { set('sentry-sampled', value); } @@ -207,6 +212,15 @@ class SentryBaggage { return double.tryParse(sampleRate); } + double? getSampleRand() { + final sampleRand = get(_sampleRandKeyName); + if (sampleRand == null) { + return null; + } + + return double.tryParse(sampleRand); + } + void setReplayId(String value) => set('sentry-replay_id', value); SentryId? getReplayId() { diff --git a/dart/lib/src/sentry_trace_context_header.dart b/dart/lib/src/sentry_trace_context_header.dart index e17b2b91f8..c80aae9fea 100644 --- a/dart/lib/src/sentry_trace_context_header.dart +++ b/dart/lib/src/sentry_trace_context_header.dart @@ -1,7 +1,7 @@ import 'package:meta/meta.dart'; -import 'protocol/sentry_id.dart'; import 'protocol/access_aware_map.dart'; +import 'protocol/sentry_id.dart'; import 'sentry_baggage.dart'; import 'sentry_options.dart'; @@ -15,6 +15,7 @@ class SentryTraceContextHeader { this.userSegment, this.transaction, this.sampleRate, + this.sampleRand, this.sampled, this.unknown, this.replayId, @@ -30,6 +31,7 @@ class SentryTraceContextHeader { final String? userSegment; final String? transaction; final String? sampleRate; + final String? sampleRand; final String? sampled; @internal @@ -102,6 +104,9 @@ class SentryTraceContextHeader { if (sampleRate != null) { baggage.setSampleRate(sampleRate!); } + if (sampleRand != null) { + baggage.setSampleRand(sampleRand!); + } if (sampled != null) { baggage.setSampled(sampled!); } @@ -113,6 +118,7 @@ class SentryTraceContextHeader { factory SentryTraceContextHeader.fromBaggage(SentryBaggage baggage) { return SentryTraceContextHeader( + // TODO: implement and use proper get methods here SentryId.fromId(baggage.get('sentry-trace_id').toString()), baggage.get('sentry-public_key').toString(), release: baggage.get('sentry-release'), diff --git a/dart/lib/src/sentry_tracer.dart b/dart/lib/src/sentry_tracer.dart index 05a1fd87d0..9dc6cb929f 100644 --- a/dart/lib/src/sentry_tracer.dart +++ b/dart/lib/src/sentry_tracer.dart @@ -385,6 +385,7 @@ class SentryTracer extends ISentrySpan { transaction: _isHighQualityTransactionName(transactionNameSource) ? name : null, sampleRate: _sampleRateToString(_rootSpan.samplingDecision?.sampleRate), + sampleRand: _sampleRandToString(_rootSpan.samplingDecision?.sampleRand), sampled: _rootSpan.samplingDecision?.sampled.toString(), ); @@ -398,6 +399,13 @@ class SentryTracer extends ISentrySpan { return sampleRate != null ? SampleRateFormat().format(sampleRate) : null; } + String? _sampleRandToString(double? sampleRand) { + if (!isValidSampleRand(sampleRand)) { + return null; + } + return sampleRand != null ? SampleRateFormat().format(sampleRand) : null; + } + bool _isHighQualityTransactionName(SentryTransactionNameSource source) { return source != SentryTransactionNameSource.url; } diff --git a/dart/lib/src/sentry_traces_sampler.dart b/dart/lib/src/sentry_traces_sampler.dart index b842514481..f668ad460f 100644 --- a/dart/lib/src/sentry_traces_sampler.dart +++ b/dart/lib/src/sentry_traces_sampler.dart @@ -31,12 +31,9 @@ class SentryTracesSampler { final tracesSampler = _options.tracesSampler; if (tracesSampler != null) { try { - final result = tracesSampler(samplingContext); - if (result != null) { - return SentryTracesSamplingDecision( - _sample(result), - sampleRate: result, - ); + final sampleRate = tracesSampler(samplingContext); + if (sampleRate != null) { + return _makeSampleDecision(sampleRate); } } catch (exception, stackTrace) { _options.logger( @@ -64,10 +61,7 @@ class SentryTracesSampler { double? optionsOrDefaultRate = optionsRate ?? defaultRate; if (optionsOrDefaultRate != null) { - return SentryTracesSamplingDecision( - _sample(optionsOrDefaultRate), - sampleRate: optionsOrDefaultRate, - ); + return _makeSampleDecision(optionsOrDefaultRate); } return SentryTracesSamplingDecision(false); @@ -78,8 +72,18 @@ class SentryTracesSampler { if (optionsRate == null || !tracesSamplingDecision.sampled) { return false; } - return _sample(optionsRate); + return _isSampled(optionsRate); } - bool _sample(double result) => !(result < _random.nextDouble()); + SentryTracesSamplingDecision _makeSampleDecision(double sampleRate) { + final sampleRand = _random.nextDouble(); + final sampled = _isSampled(sampleRate, sampleRand: sampleRand); + return SentryTracesSamplingDecision(sampled, + sampleRate: sampleRate, sampleRand: sampleRand); + } + + bool _isSampled(double sampleRate, {double? sampleRand}) { + final rand = sampleRand ?? _random.nextDouble(); + return rand <= sampleRate; + } } diff --git a/dart/lib/src/sentry_traces_sampling_decision.dart b/dart/lib/src/sentry_traces_sampling_decision.dart index 90161515cf..802d27f832 100644 --- a/dart/lib/src/sentry_traces_sampling_decision.dart +++ b/dart/lib/src/sentry_traces_sampling_decision.dart @@ -2,8 +2,10 @@ class SentryTracesSamplingDecision { SentryTracesSamplingDecision( this.sampled, { this.sampleRate, + this.sampleRand, }); final bool sampled; final double? sampleRate; + final double? sampleRand; } diff --git a/dart/lib/src/sentry_transaction_context.dart b/dart/lib/src/sentry_transaction_context.dart index 32ab0324b7..9caa170385 100644 --- a/dart/lib/src/sentry_transaction_context.dart +++ b/dart/lib/src/sentry_transaction_context.dart @@ -1,27 +1,27 @@ import 'package:meta/meta.dart'; -import 'sentry_trace_origins.dart'; import 'protocol.dart'; import 'sentry_baggage.dart'; +import 'sentry_trace_origins.dart'; import 'tracing.dart'; @immutable class SentryTransactionContext extends SentrySpanContext { final String name; - final SentryTracesSamplingDecision? parentSamplingDecision; final SentryTransactionNameSource? transactionNameSource; final SentryTracesSamplingDecision? samplingDecision; + final SentryTracesSamplingDecision? parentSamplingDecision; SentryTransactionContext( this.name, String operation, { super.description, - this.parentSamplingDecision, super.traceId, super.spanId, super.parentSpanId, this.transactionNameSource, this.samplingDecision, + this.parentSamplingDecision, super.origin, }) : super( operation: operation, @@ -35,6 +35,7 @@ class SentryTransactionContext extends SentrySpanContext { SentryBaggage? baggage, }) { final sampleRate = baggage?.getSampleRate(); + final sampleRand = baggage?.getSampleRand(); return SentryTransactionContext( name, operation, @@ -44,6 +45,7 @@ class SentryTransactionContext extends SentrySpanContext { ? SentryTracesSamplingDecision( traceHeader.sampled!, sampleRate: sampleRate, + sampleRand: sampleRand, ) : null, transactionNameSource: diff --git a/dart/lib/src/utils/tracing_utils.dart b/dart/lib/src/utils/tracing_utils.dart index 6198062ddc..5a14311d1c 100644 --- a/dart/lib/src/utils/tracing_utils.dart +++ b/dart/lib/src/utils/tracing_utils.dart @@ -80,3 +80,10 @@ bool isValidSampleRate(double? sampleRate) { } return !sampleRate.isNaN && sampleRate >= 0.0 && sampleRate <= 1.0; } + +bool isValidSampleRand(double? sampleRand) { + if (sampleRand == null) { + return false; + } + return !sampleRand.isNaN && sampleRand >= 0.0 && sampleRand < 1.0; +} diff --git a/dart/test/protocol/sentry_baggage_header_test.dart b/dart/test/protocol/sentry_baggage_header_test.dart index 910929776e..6d75d6dd15 100644 --- a/dart/test/protocol/sentry_baggage_header_test.dart +++ b/dart/test/protocol/sentry_baggage_header_test.dart @@ -21,6 +21,7 @@ void main() { baggage.setUserSegment('userSegment'); baggage.setTransaction('transaction'); baggage.setSampleRate('1.0'); + baggage.setSampleRand('0.4'); baggage.setSampled('false'); final replayId = SentryId.newId().toString(); baggage.setReplayId(replayId); @@ -37,6 +38,7 @@ void main() { 'sentry-user_segment=userSegment,' 'sentry-transaction=transaction,' 'sentry-sample_rate=1.0,' + 'sentry-sample_rand=0.4,' 'sentry-sampled=false,' 'sentry-replay_id=$replayId'); }); diff --git a/dart/test/sentry_tracer_test.dart b/dart/test/sentry_tracer_test.dart index 667e43b159..d5dde5ae98 100644 --- a/dart/test/sentry_tracer_test.dart +++ b/dart/test/sentry_tracer_test.dart @@ -487,6 +487,7 @@ void main() { SentryTracesSamplingDecision( true, sampleRate: 1.0, + sampleRand: 0.8, ); final _context = SentryTransactionContext( 'name', @@ -512,6 +513,7 @@ void main() { expect(newBaggage.get('sentry-user_segment'), 'segment'); expect(newBaggage.get('sentry-transaction'), 'name'); expect(newBaggage.get('sentry-sample_rate'), '1'); + expect(newBaggage.getSampleRand(), 0.8); expect(newBaggage.get('sentry-sampled'), 'true'); });