Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1a4b78b
Add TelemetryProcessor for span and log buffering
buenaflor Jan 14, 2026
3897122
Remove SpanV2 and TraceLifecycle dependencies
buenaflor Jan 14, 2026
2c617bb
Remove span-related tests from sentry_client_test
buenaflor Jan 14, 2026
6ef8c3c
Remove span-related processor tests
buenaflor Jan 14, 2026
3ca4c08
Remove span import from Flutter mocks
buenaflor Jan 14, 2026
9b34042
Fix wiring up
buenaflor Jan 14, 2026
e0b564c
Update
buenaflor Jan 14, 2026
6da49c8
Update
buenaflor Jan 14, 2026
1b97198
Update CHANGELOG
buenaflor Jan 14, 2026
82a4374
Update
buenaflor Jan 14, 2026
1a48756
Update
buenaflor Jan 14, 2026
1081ca3
Update
buenaflor Jan 14, 2026
ffd9fc7
Update
buenaflor Jan 15, 2026
b56a272
Update
buenaflor Jan 15, 2026
586ae3d
Update
buenaflor Jan 15, 2026
26d0577
Merge branch 'main' into feat/metrics
buenaflor Jan 15, 2026
1f7ba8f
Merge branch 'main' into feat/metrics
buenaflor Jan 17, 2026
33f991f
feat: integrate telemetry metrics into Sentry options and core functiโ€ฆ
buenaflor Jan 17, 2026
b6bff0d
feat: implement SentryMetrics with Default and NoOp implementations
buenaflor Jan 17, 2026
95fb48d
Merge branch 'main' into feat/metrics
buenaflor Jan 19, 2026
435917d
feat: enhance telemetry metrics with MetricCapturePipeline and defaulโ€ฆ
buenaflor Jan 19, 2026
07b5051
Add more tests
buenaflor Jan 19, 2026
875ca84
feat: enhance metric capturing and logging in MetricCapturePipeline aโ€ฆ
buenaflor Jan 20, 2026
b5eee19
feat: enhance Sentry attribute formatting and replay integration
buenaflor Jan 20, 2026
61bb4bc
feat: include unit in DefaultSentryMetrics and update tests
buenaflor Jan 20, 2026
5b55d0a
fix: update metric logging to use processed metrics
buenaflor Jan 20, 2026
264f72b
Fix missing spec
buenaflor Jan 20, 2026
f49e371
Update CHANGELOG
buenaflor Jan 20, 2026
5cfeba6
Add examples
buenaflor Jan 20, 2026
ee01dbb
Merge branch 'main' into feat/metrics
buenaflor Jan 20, 2026
fbfcc74
Update doc
buenaflor Jan 20, 2026
c7ca90c
Update doc
buenaflor Jan 20, 2026
7f3b491
Update
buenaflor Jan 20, 2026
c4221aa
Update
buenaflor Jan 20, 2026
2a553a2
Update docs
buenaflor Jan 20, 2026
ba9930c
Add support for 'trace_metric' category in RateLimitParser and corresโ€ฆ
buenaflor Jan 20, 2026
e3e55a5
Update comments
buenaflor Jan 20, 2026
e92efaf
Add SentryMetricUnit constants
buenaflor Jan 20, 2026
d530afc
Fix compilation
buenaflor Jan 20, 2026
eb2cad9
Fix analyze
buenaflor Jan 20, 2026
3282f7a
Update
buenaflor Jan 20, 2026
71e67b7
Refactor SentryMetrics handling in SentryOptions to use a private varโ€ฆ
buenaflor Jan 20, 2026
cd836b5
Refactor SentryMetrics in SentryOptions to use a late variable for imโ€ฆ
buenaflor Jan 20, 2026
2648213
Add close
buenaflor Jan 20, 2026
98f48fd
Add close test
buenaflor Jan 20, 2026
747259d
Update SentryMetrics initialization in SentryOptions to use a constanโ€ฆ
buenaflor Jan 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

### Features

- Trace connected metrics ([#3450](https://github.com/getsentry/sentry-dart/pull/3450))
- This feature is enabled by default.
- To send metrics use the following APIs:
- `Sentry.metrics.gauge(...)`
- `Sentry.metrics.count(...)`
- `Sentry.metrics.distribution(...)`

### Dependencies

- Bump Android SDK from v8.28.0 to v8.30.0 ([#3451](https://github.com/getsentry/sentry-dart/pull/3451))
Expand Down
1 change: 1 addition & 0 deletions packages/dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ export 'src/utils/url_details.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/breadcrumb_log_level.dart';
export 'src/sentry_logger.dart';
export 'src/telemetry/metric/metrics.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/internal_logger.dart' show SentryInternalLogger;
2 changes: 2 additions & 0 deletions packages/dart/lib/src/client_reports/discarded_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ extension _DataCategoryExtension on DataCategory {
return 'feedback';
case DataCategory.metricBucket:
return 'metric_bucket';
case DataCategory.metric:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other types are called like the raw value, so in this case it should be 'traceMetric'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep it as metrics as per spec:

The envelope item type is named trace_metric for internal usage to avoid naming collisions with other metric systems within Sentry's infrastructure. From an SDK perspective, these are simply referred to as "metrics".

return 'trace_metric';
}
}
}
77 changes: 77 additions & 0 deletions packages/dart/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,80 @@ class SentrySpanDescriptions {
static String dbOpen({required String dbName}) => 'Open database $dbName';
static String dbClose({required String dbName}) => 'Close database $dbName';
}

/// Semantic attributes for telemetry.
///
/// Not all attributes apply to every telemetry type.
///
/// See https://getsentry.github.io/sentry-conventions/generated/attributes/
/// for more details.
@internal
abstract class SemanticAttributesConstants {
SemanticAttributesConstants._();

/// The source of a span, also referred to as transaction source.
///
/// Known values are: `'custom'`, `'url'`, `'route'`, `'component'`, `'view'`, `'task'`.
static const sentrySpanSource = 'sentry.span.source';

/// Attributes that holds the sample rate that was locally applied to a span.
/// If this attribute is not defined, it means that the span inherited a sampling decision.
///
/// NOTE: Is only defined on root spans.
static const sentrySampleRate = 'sentry.sample_rate';

/// Use this attribute to represent the origin of a span.
static const sentryOrigin = 'sentry.origin';

/// The release version of the application
static const sentryRelease = 'sentry.release';

/// The environment name (e.g., "production", "staging", "development")
static const sentryEnvironment = 'sentry.environment';

/// The segment name (e.g., "GET /users")
static const sentrySegmentName = 'sentry.segment.name';

/// The span id of the segment that this span belongs to.
static const sentrySegmentId = 'sentry.segment.id';

/// The name of the Sentry SDK (e.g., "sentry.dart.flutter")
static const sentrySdkName = 'sentry.sdk.name';

/// The version of the Sentry SDK
static const sentrySdkVersion = 'sentry.sdk.version';

/// The replay ID.
static const sentryReplayId = 'sentry.replay_id';

/// Whether the replay is buffering (onErrorSampleRate).
static const sentryInternalReplayIsBuffering =
'sentry._internal.replay_is_buffering';

/// The user ID (gated by `sendDefaultPii`).
static const userId = 'user.id';

/// The user email (gated by `sendDefaultPii`).
static const userEmail = 'user.email';

/// The user IP address (gated by `sendDefaultPii`).
static const userIpAddress = 'user.ip_address';

/// The user username (gated by `sendDefaultPii`).
static const userName = 'user.name';

/// The operating system name.
static const osName = 'os.name';

/// The operating system version.
static const osVersion = 'os.version';

/// The device brand (e.g., "Apple", "Samsung").
static const deviceBrand = 'device.brand';

/// The device model identifier (e.g., "iPhone14,2").
static const deviceModel = 'device.model';

/// The device family (e.g., "iOS", "Android").
static const deviceFamily = 'device.family';
}
33 changes: 33 additions & 0 deletions packages/dart/lib/src/hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'client_reports/discard_reason.dart';
import 'profiling.dart';
import 'sentry_tracer.dart';
import 'sentry_traces_sampler.dart';
import 'telemetry/metric/metric.dart';
import 'transport/data_category.dart';

/// Configures the scope through the callback.
Expand Down Expand Up @@ -317,6 +318,38 @@ class Hub {
}
}

Future<void> captureMetric(SentryMetric metric) async {
if (!_isEnabled) {
_options.log(
SentryLevel.warning,
"Instance is disabled and this 'captureMetric' call is a no-op.",
);
} else {
final item = _peek();
late Scope scope;
final s = _cloneAndRunWithScope(item.scope, null);
if (s is Future<Scope>) {
scope = await s;
} else {
scope = s;
}

try {
await item.client.captureMetric(
metric,
scope: scope,
);
} catch (exception, stacktrace) {
_options.log(
SentryLevel.error,
'Error while capturing metric',
exception: exception,
stackTrace: stacktrace,
);
}
}
}

FutureOr<Scope> _cloneAndRunWithScope(
Scope scope, ScopeCallback? withScope) async {
if (withScope != null) {
Expand Down
5 changes: 5 additions & 0 deletions packages/dart/lib/src/hub_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'scope.dart';
import 'sentry.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'telemetry/metric/metric.dart';
import 'tracing.dart';

/// Hub adapter to make Integrations testable
Expand Down Expand Up @@ -200,6 +201,10 @@ class HubAdapter implements Hub {
@override
FutureOr<void> captureLog(SentryLog log) => Sentry.currentHub.captureLog(log);

@override
Future<void> captureMetric(SentryMetric metric) =>
Sentry.currentHub.captureMetric(metric);

@override
void setAttributes(Map<String, SentryAttribute> attributes) =>
Sentry.currentHub.setAttributes(attributes);
Expand Down
4 changes: 4 additions & 0 deletions packages/dart/lib/src/noop_hub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'protocol/sentry_feedback.dart';
import 'scope.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'telemetry/metric/metric.dart';
import 'tracing.dart';

class NoOpHub implements Hub {
Expand Down Expand Up @@ -97,6 +98,9 @@ class NoOpHub implements Hub {
@override
FutureOr<void> captureLog(SentryLog log) async {}

@override
Future<void> captureMetric(SentryMetric metric) async {}

@override
ISentrySpan startTransaction(
String name,
Expand Down
4 changes: 4 additions & 0 deletions packages/dart/lib/src/noop_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'scope.dart';
import 'sentry_client.dart';
import 'sentry_envelope.dart';
import 'sentry_trace_context_header.dart';
import 'telemetry/metric/metric.dart';

class NoOpSentryClient implements SentryClient {
NoOpSentryClient._();
Expand Down Expand Up @@ -69,4 +70,7 @@ class NoOpSentryClient implements SentryClient {

@override
FutureOr<void> captureLog(SentryLog log, {Scope? scope}) async {}

@override
Future<void> captureMetric(SentryMetric metric, {Scope? scope}) async {}
}
8 changes: 8 additions & 0 deletions packages/dart/lib/src/sdk_lifecycle_hooks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:meta/meta.dart';

import '../sentry.dart';
import 'telemetry/metric/metric.dart';

@internal
typedef SdkLifecycleCallback<T extends SdkLifecycleEvent> = FutureOr<void>
Expand Down Expand Up @@ -96,3 +97,10 @@ class OnSpanFinish extends SdkLifecycleEvent {

final ISentrySpan span;
}

@internal
class OnProcessMetric extends SdkLifecycleEvent {
final SentryMetric metric;

OnProcessMetric(this.metric);
}
5 changes: 5 additions & 0 deletions packages/dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import 'sentry_attachment/sentry_attachment.dart';
import 'sentry_client.dart';
import 'sentry_options.dart';
import 'sentry_run_zoned_guarded.dart';
import 'telemetry/metric/metrics_setup_integration.dart';
import 'telemetry/metric/metrics.dart';
import 'telemetry/processing/processor_integration.dart';
import 'tracing.dart';
import 'transport/data_category.dart';
Expand Down Expand Up @@ -110,6 +112,7 @@ class Sentry {
options.addIntegration(LoadDartDebugImagesIntegration());
}

options.addIntegration(MetricsSetupIntegration());
options.addIntegration(FeatureFlagsIntegration());
options.addIntegration(LogsEnricherIntegration());
options.addIntegration(InMemoryTelemetryProcessorIntegration());
Expand Down Expand Up @@ -450,4 +453,6 @@ class Sentry {
);

static SentryLogger get logger => currentHub.options.logger;

static SentryMetrics get metrics => currentHub.options.metrics;
}
14 changes: 11 additions & 3 deletions packages/dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import 'sentry_exception_factory.dart';
import 'sentry_options.dart';
import 'sentry_stack_trace_factory.dart';
import 'sentry_trace_context_header.dart';
import 'telemetry/metric/metric.dart';
import 'telemetry/metric/metric_capture_pipeline.dart';
import 'transport/client_report_transport.dart';
import 'transport/data_category.dart';
import 'transport/http_transport.dart';
Expand All @@ -42,14 +44,16 @@ String get defaultIpAddress => _defaultIpAddress;
class SentryClient {
final SentryOptions _options;
final Random? _random;
final MetricCapturePipeline _metricCapturePipeline;

static final _emptySentryId = Future.value(SentryId.empty());

SentryExceptionFactory get _exceptionFactory => _options.exceptionFactory;
SentryStackTraceFactory get _stackTraceFactory => _options.stackTraceFactory;

/// Instantiates a client using [SentryOptions]
factory SentryClient(SentryOptions options) {
factory SentryClient(SentryOptions options,
{MetricCapturePipeline? metricCapturePipeline}) {
if (options.sendClientReports) {
options.recorder = ClientReportRecorder(options.clock);
}
Expand All @@ -74,11 +78,12 @@ class SentryClient {
if (enableFlutterSpotlight) {
options.transport = SpotlightHttpTransport(options, options.transport);
}
return SentryClient._(options);
return SentryClient._(
options, metricCapturePipeline ?? MetricCapturePipeline(options));
}

/// Instantiates a client using [SentryOptions]
SentryClient._(this._options)
SentryClient._(this._options, this._metricCapturePipeline)
: _random = _options.sampleRate == null ? null : Random();

/// Reports an [event] to Sentry.io.
Expand Down Expand Up @@ -583,6 +588,9 @@ class SentryClient {
}
}

Future<void> captureMetric(SentryMetric metric, {Scope? scope}) =>
_metricCapturePipeline.captureMetric(metric, scope: scope);

FutureOr<void> close() {
final flush = _options.telemetryProcessor.flush();
if (flush is Future<void>) {
Expand Down
61 changes: 37 additions & 24 deletions packages/dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,30 +104,29 @@ class SentryEnvelope {
factory SentryEnvelope.fromLogsData(
List<List<int>> encodedLogs,
SdkVersion sdkVersion,
) {
// Create the payload in the format expected by Sentry
// Format: {"items": [log1, log2, ...]}
final builder = BytesBuilder(copy: false);
builder.add(utf8.encode('{"items":['));
for (int i = 0; i < encodedLogs.length; i++) {
if (i > 0) {
builder.add(utf8.encode(','));
}
builder.add(encodedLogs[i]);
}
builder.add(utf8.encode(']}'));

return SentryEnvelope(
SentryEnvelopeHeader(
null,
sdkVersion,
),
[
SentryEnvelopeItem.fromLogsData(
builder.takeBytes(), encodedLogs.length),
],
);
}
) =>
SentryEnvelope(
SentryEnvelopeHeader(null, sdkVersion),
[
SentryEnvelopeItem.fromLogsData(
_buildItemsPayload(encodedLogs), encodedLogs.length)
],
);

/// Create a [SentryEnvelope] containing raw metric data payload.
/// This is used by the log batcher to send pre-encoded metric batches.
@internal
factory SentryEnvelope.fromMetricsData(
List<List<int>> encodedMetrics,
SdkVersion sdkVersion,
) =>
SentryEnvelope(
SentryEnvelopeHeader(null, sdkVersion),
[
SentryEnvelopeItem.fromMetricsData(
_buildItemsPayload(encodedMetrics), encodedMetrics.length)
],
);

/// Stream binary data representation of `Envelope` file encoded.
Stream<List<int>> envelopeStream(SentryOptions options) async* {
Expand Down Expand Up @@ -160,6 +159,20 @@ class SentryEnvelope {
}
}

/// Builds a payload in the format {"items": [item1, item2, ...]}
static Uint8List _buildItemsPayload(List<List<int>> encodedItems) {
final builder = BytesBuilder(copy: false);
builder.add(utf8.encode('{"items":['));
for (int i = 0; i < encodedItems.length; i++) {
if (i > 0) {
builder.add(utf8.encode(','));
}
builder.add(encodedItems[i]);
}
builder.add(utf8.encode(']}'));
return builder.takeBytes();
}
Comment on lines +162 to +174
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is taken from the span-first branch


/// Add an envelope item containing client report data.
void addClientReport(ClientReport? clientReport) {
if (clientReport != null) {
Expand Down
Loading
Loading