Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e0269c3
feat(grpc): Add sentry_grpc instrumentation package
lucas-zimerman May 20, 2026
9a83568
feat(grpc): Add example apps for grpc 4 and grpc 5
lucas-zimerman May 20, 2026
94651a7
feat(grpc): Add DummyUnary and RandomError to example apps
lucas-zimerman May 21, 2026
ba3e62b
sample app cleanup
lucas-zimerman May 21, 2026
53925f2
sample app cleanup
lucas-zimerman May 21, 2026
05509c1
unused transaction removal
lucas-zimerman May 21, 2026
247fbe6
feat(grpc): Add headers and error details to spans
lucas-zimerman May 27, 2026
5e7efa0
docs(grpc): Add example app README files
lucas-zimerman May 27, 2026
f5d1d92
chore(grpc): Add android .gitignore for grpc4 example
lucas-zimerman May 27, 2026
c84cccc
test(grpc): Add captureRequestHeaders and PII header filter tests
lucas-zimerman May 27, 2026
cf2eeaf
chore(grpc): Add app_config.dart for grpc5 example
lucas-zimerman May 27, 2026
1ea8728
refactor(grpc): Convert example apps to Dart-only CLI
lucas-zimerman May 27, 2026
3932a15
chore(grpc): Gitignore .metadata in example apps
lucas-zimerman May 27, 2026
a4d676d
Merge branch 'main' into lz/grpc
lucas-zimerman May 27, 2026
0cb3cb8
add grpc to bump-version
lucas-zimerman May 27, 2026
a0666ee
Merge branch 'lz/grpc' of https://github.com/getsentry/sentry-dart inโ€ฆ
lucas-zimerman May 27, 2026
311bc70
fix(grpc): Address PR review comments
lucas-zimerman May 28, 2026
166b609
fix(grpc): Fix trailing commas in _attachErrorDetails
lucas-zimerman May 28, 2026
0623fa7
feat(grpc): Use OTEL gRPC semantic convention attribute names
lucas-zimerman May 28, 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
55 changes: 55 additions & 0 deletions packages/dart/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,61 @@
/// The database name.
// TODO: deprecated, needs to be replaced later by db.namespace
static const dbName = 'db.name';

/// The RPC system identifier. For gRPC, always `'grpc'`.
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
static const rpcSystemName = 'rpc.system.name';

Check warning on line 421 in packages/dart/lib/src/constants.dart

View check run for this annotation

@sentry/warden / warden: code-review

[B83-A56] rpcRequestMetadataPrefix uses 'rpc.request.metadata.' instead of OTel-specified 'rpc.grpc.request.metadata.' (additional location)

The OTel gRPC spec (linked in the doc comment) scopes request metadata under `rpc.grpc.request.metadata.<key>`; the current prefix `rpc.request.metadata.` is non-standard and breaks backend interoperability.

Check warning on line 421 in packages/dart/lib/src/constants.dart

View check run for this annotation

@sentry/warden / warden: find-bugs

`rpcSystemName` string value diverges from OTel spec (`'rpc.system.name'` vs `'rpc.system'`)

The constant value is `'rpc.system.name'` but the OpenTelemetry RPC semantic convention (cited in the doc comment) defines this attribute as `rpc.system`. Spans emitted with this key will not be recognized by OTel-compatible backends (Grafana, Jaeger, etc.) that filter or group by `rpc.system`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rpcSystemName string value diverges from OTel spec ('rpc.system.name' vs 'rpc.system')

The constant value is 'rpc.system.name' but the OpenTelemetry RPC semantic convention (cited in the doc comment) defines this attribute as rpc.system. Spans emitted with this key will not be recognized by OTel-compatible backends (Grafana, Jaeger, etc.) that filter or group by rpc.system.

Evidence
  • OTel RPC semconv (https://opentelemetry.io/docs/specs/semconv/rpc/grpc/) defines the attribute as rpc.system, not rpc.system.name.
  • The doc comment directly links to that spec page yet the string value 'rpc.system.name' is incorrect.
  • _attachRpcAttributes in sentry_grpc_interceptor.dart calls span.setData(SemanticAttributesConstants.rpcSystemName, 'grpc'), so spans in production will carry the non-standard key.
  • Tests assert against SemanticAttributesConstants.rpcSystemName (line 115 in test file), so they pass but validate the wrong key.

Suggested fix: Change the string value to the correct OTel attribute name.

Suggested change
static const rpcSystemName = 'rpc.system.name';
static const rpcSystemName = 'rpc.system';

Identified by Warden find-bugs ยท U3Z-TQL

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

rpc.name doesn't exist


/// The fully-qualified logical name of the method (`ServiceName/MethodName`).
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
static const rpcMethod = 'rpc.method';

/// The string representation of the gRPC status code (e.g., `'OK'`, `'NOT_FOUND'`).
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
static const rpcResponseStatusCode = 'rpc.response.status_code';

Check warning on line 431 in packages/dart/lib/src/constants.dart

View check run for this annotation

@sentry/warden / warden: find-bugs

`rpcResponseStatusCode` string value diverges from OTel spec (`'rpc.response.status_code'` vs `'rpc.grpc.status_code'`)

The constant value is `'rpc.response.status_code'` but the OTel gRPC semantic convention (cited in the doc comment) defines the gRPC status code attribute as `rpc.grpc.status_code`. This will emit a non-standard key that OTel tooling won't map to gRPC status metrics or dashboards.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rpcResponseStatusCode string value diverges from OTel spec ('rpc.response.status_code' vs 'rpc.grpc.status_code')

The constant value is 'rpc.response.status_code' but the OTel gRPC semantic convention (cited in the doc comment) defines the gRPC status code attribute as rpc.grpc.status_code. This will emit a non-standard key that OTel tooling won't map to gRPC status metrics or dashboards.

Evidence
  • OTel RPC/gRPC semconv specifies rpc.grpc.status_code for the gRPC status code; rpc.response.status_code is not defined in that spec.
  • The doc comment says 'See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/' but the string contradicts it.
  • interceptUnary in sentry_grpc_interceptor.dart calls span.setData(SemanticAttributesConstants.rpcResponseStatusCode, 'OK') and similar on the error path (lines 113 and 133), propagating the wrong key on every span.
  • Tests at lines 134 and 259 validate against the constant, so they pass but verify the non-standard key.

Suggested fix: Change the string value to the correct OTel attribute name.

Suggested change
static const rpcResponseStatusCode = 'rpc.response.status_code';
static const rpcResponseStatusCode = 'rpc.grpc.status_code';

Identified by Warden find-bugs ยท GCT-9GL

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

rpc.grpc.status_code is not there, as odd as it sounds, they adopted to use a general key name from rpc instead of adding grpc to the key name


/// Prefix for RPC request metadata. Append the lowercase metadata key.
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/

Check warning on line 435 in packages/dart/lib/src/constants.dart

View check run for this annotation

@sentry/warden / warden: code-review

rpcRequestMetadataPrefix uses 'rpc.request.metadata.' instead of OTel-specified 'rpc.grpc.request.metadata.'

The OTel gRPC spec (linked in the doc comment) scopes request metadata under `rpc.grpc.request.metadata.<key>`; the current prefix `rpc.request.metadata.` is non-standard and breaks backend interoperability.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

rpcRequestMetadataPrefix uses 'rpc.request.metadata.' instead of OTel-specified 'rpc.grpc.request.metadata.'

The OTel gRPC spec (linked in the doc comment) scopes request metadata under rpc.grpc.request.metadata.<key>; the current prefix rpc.request.metadata. is non-standard and breaks backend interoperability.

Evidence
  • Doc comment on line 433 links to the OTel gRPC semconv.
  • OTel gRPC spec defines metadata attributes as rpc.grpc.request.metadata.<key>.
  • Constant value is 'rpc.request.metadata.' (line 435), missing the grpc. infix.
  • _attachRequestData in the interceptor constructs keys as ${rpcRequestMetadataPrefix}$normalizedKey, so all captured metadata headers will have the wrong OTel key prefix.
Also found at 1 additional location
  • packages/dart/lib/src/constants.dart:421

Identified by Warden code-review ยท B83-A56

static const rpcRequestMetadataPrefix = 'rpc.request.metadata.';

/// The reason string from a gRPC ErrorInfo error detail.
static const grpcErrorInfoReason = 'grpc.error_info.reason';

/// The domain string from a gRPC ErrorInfo error detail.
static const grpcErrorInfoDomain = 'grpc.error_info.domain';

/// The metadata map from a gRPC ErrorInfo error detail.
static const grpcErrorInfoMetadata = 'grpc.error_info.metadata';

/// The field violations from a gRPC BadRequest error detail.
static const grpcBadRequestFieldViolations =
'grpc.bad_request.field_violations';

/// The retry delay from a gRPC RetryInfo error detail.
static const grpcRetryInfoRetryDelay = 'grpc.retry_info.retry_delay';

/// The detail string from a gRPC DebugInfo error detail.
static const grpcDebugInfoDetail = 'grpc.debug_info.detail';

/// The violations from a gRPC PreconditionFailure error detail.
static const grpcPreconditionFailureViolations =
'grpc.precondition_failure.violations';

/// The resource type from a gRPC ResourceInfo error detail.
static const grpcResourceInfoType = 'grpc.resource_info.type';

/// The resource name from a gRPC ResourceInfo error detail.
static const grpcResourceInfoName = 'grpc.resource_info.name';

/// The description from a gRPC ResourceInfo error detail.
static const grpcResourceInfoDescription = 'grpc.resource_info.description';

/// The violations from a gRPC QuotaFailure error detail.
static const grpcQuotaFailureViolations = 'grpc.quota_failure.violations';
}

/// Attribute keys emitted by the SDK that are not (yet) codified in
Expand Down
1 change: 1 addition & 0 deletions packages/dart/lib/src/sentry_trace_origins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SentryTraceOrigins {
'auto.http.dio.http_client_adapter';
static const autoHttpDioTransformer = 'auto.http.dio.transformer';
static const autoGraphQlSentryLink = 'auto.graphql.sentry_link';
static const autoGrpcClientInterceptor = 'auto.grpc.client_interceptor';
static const autoFile = 'auto.file';
static const autoFileAssetBundle = 'auto.file.asset_bundle';
static const autoDbSqfliteOpenDatabase = 'auto.db.sqflite.open_database';
Expand Down
1 change: 1 addition & 0 deletions packages/grpc/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Changelog
27 changes: 27 additions & 0 deletions packages/grpc/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
include: package:lints/recommended.yaml

analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
missing_required_param: error
missing_return: error
todo: ignore
deprecated_member_use_from_same_package: warning
invalid_dependency: ignore
unnecessary_import: ignore
exclude:
- example/**

linter:
rules:
- prefer_final_locals
- public_member_api_docs
- prefer_single_quotes
- prefer_relative_imports
- unnecessary_brace_in_string_interps
- implementation_imports
- require_trailing_commas
- unawaited_futures
54 changes: 54 additions & 0 deletions packages/grpc/example/grpc4/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Platform folders (Dart-only example, no platform targets committed)
/android/
/ios/
/linux/
/macos/
/web/
/windows/

# Breadcrumb and Event cache.
.sentry-native

# Flutter tool metadata (not needed for building).
.metadata
34 changes: 34 additions & 0 deletions packages/grpc/example/grpc4/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# grpc4 โ€” sentry_grpc Dart example

Dart CLI example demonstrating `SentryGrpcInterceptor` with grpc 4.x.

## What it shows

- `Sentry.init` with `tracesSampleRate` and `captureFailedRequests`
- `SentryGrpcInterceptor` attached to a `ClientChannel` targeting `grpcb.in:9001`
- Manual transaction wrapping around each RPC call

## Calls

| Call | Endpoint | Purpose |
|------|----------|---------|
| Empty | `GRPCBin/Empty` | Successful unary RPC; creates a span |
| DummyUnary | `GRPCBin/DummyUnary` | Sends a string, echoes it back (hand-encoded proto) |
| RandomError | `GRPCBin/RandomError` | Randomly fails; tests error span + capture |

## Differences from grpc5

| | grpc4 | grpc5 |
|-|-------|-------|
| grpc version | 4.x | 5.x |
| Proto encoding | Inline byte helpers | Typed `DummyMessage` class |
| WithHeaders call | No | Yes |

## Run

```sh
cd packages/grpc/example/grpc4
dart run lib/main.dart
```

Set your DSN in `lib/app_config.dart` before running.
1 change: 1 addition & 0 deletions packages/grpc/example/grpc4/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:lints/recommended.yaml
3 changes: 3 additions & 0 deletions packages/grpc/example/grpc4/lib/app_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// ATTENTION: Change the DSN below with your own to see the events in Sentry. Get one at sentry.io
const String exampleDsn =
'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562';
148 changes: 148 additions & 0 deletions packages/grpc/example/grpc4/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import 'dart:convert';

import 'package:grpc/grpc.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry_grpc/sentry_grpc.dart';

import 'app_config.dart' as config;

Future<void> main() async {
await Sentry.init(
(options) {
options.dsn = config.exampleDsn;
options.tracesSampleRate = 1.0;
options.captureFailedRequests = true;
options.debug = true;
},
appRunner: _runApp,
);
}

Future<void> _runApp() async {
final channel = ClientChannel(
'grpcb.in',
port: 9001,
options: const ChannelOptions(credentials: ChannelCredentials.secure()),
);
final client = _GrpcBinClient(
channel,
interceptors: [SentryGrpcInterceptor(captureFailedRequests: true)],
);

try {
await _emptyCall(client);
await _dummyUnaryCall(client);
await _randomErrorCall(client);
} finally {
await channel.shutdown();
await Sentry.close();
}
}

Future<void> _emptyCall(_GrpcBinClient client) async {
print('--- GRPCBin/Empty ---');
final tr = Sentry.startTransaction(
'grpcb-empty',
'grpc.client',
bindToScope: true,
);
try {
await client.empty();
tr.status = const SpanStatus.ok();
print('OK');
} catch (e, s) {
tr.throwable = e;
tr.status = const SpanStatus.internalError();
await Sentry.captureException(e, stackTrace: s);
print('FAILED: $e');
} finally {
await tr.finish();
}
}

Future<void> _dummyUnaryCall(_GrpcBinClient client) async {
print('--- GRPCBin/DummyUnary ---');
final tr = Sentry.startTransaction(
'grpcb-dummy-unary',
'grpc.client',
bindToScope: true,
);
try {
final response = await client.dummyUnary(
_encodeDummyMessage('hello from sentry_grpc'),
);
tr.status = const SpanStatus.ok();
print('echo: "${_decodeDummyFString(response)}"');
} catch (e, s) {
tr.throwable = e;
tr.status = const SpanStatus.internalError();
await Sentry.captureException(e, stackTrace: s);
print('FAILED: $e');
} finally {
await tr.finish();
}
}

Future<void> _randomErrorCall(_GrpcBinClient client) async {
print('--- GRPCBin/RandomError ---');
final tr = Sentry.startTransaction(
'grpcb-random-error',
'grpc.client',
bindToScope: true,
);
try {
await client.randomError();
tr.status = const SpanStatus.ok();
print('OK');
} catch (e, s) {
tr.throwable = e;
tr.status = const SpanStatus.internalError();
await Sentry.captureException(e, stackTrace: s);
print('FAILED: $e');
} finally {
await tr.finish();
}
}

List<int> _encodeDummyMessage(String value) {
final bytes = utf8.encode(value);
return [0x0A, bytes.length, ...bytes];
}

String _decodeDummyFString(List<int> bytes) {
if (bytes.length < 2 || bytes[0] != 0x0A) return '(no f_string)';
final len = bytes[1];
if (bytes.length < 2 + len) return '(truncated)';
return utf8.decode(bytes.sublist(2, 2 + len));
}

class _GrpcBinClient extends Client {
static final _emptyCall = ClientMethod<List<int>, List<int>>(
'/grpcbin.GRPCBin/Empty',
(data) => data,
(data) => data,
);

static final _dummyUnaryCall = ClientMethod<List<int>, List<int>>(
'/grpcbin.GRPCBin/DummyUnary',
(data) => data,
(data) => data,
);

static final _randomErrorCall = ClientMethod<List<int>, List<int>>(
'/grpcbin.GRPCBin/RandomError',
(data) => data,
(data) => data,
);

_GrpcBinClient(super.channel, {super.interceptors});

ResponseFuture<List<int>> empty() =>
$createUnaryCall(_emptyCall, const <int>[]);

ResponseFuture<List<int>> dummyUnary(List<int> request) =>
$createUnaryCall(_dummyUnaryCall, request);

ResponseFuture<List<int>> randomError() =>
$createUnaryCall(_randomErrorCall, const <int>[]);
}
16 changes: 16 additions & 0 deletions packages/grpc/example/grpc4/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: sentry_grpc_example_grpc4
description: sentry_grpc example using grpc 4.x
version: 1.0.0+1
publish_to: none

environment:
sdk: '>=3.5.0 <4.0.0'

dependencies:
grpc: '>=4.0.4 <5.0.0'
sentry: 9.20.0
sentry_grpc:
path: ../..

dev_dependencies:
lints: ^5.0.0
5 changes: 5 additions & 0 deletions packages/grpc/example/grpc4/pubspec_overrides.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dependency_overrides:
sentry:
path: ../../../dart
sentry_grpc:
path: ../..
Loading
Loading