Skip to content

feat(grpc): Add integration support for GRPC#3721

Draft
lucas-zimerman wants to merge 19 commits into
mainfrom
lz/grpc
Draft

feat(grpc): Add integration support for GRPC#3721
lucas-zimerman wants to merge 19 commits into
mainfrom
lz/grpc

Conversation

@lucas-zimerman
Copy link
Copy Markdown
Collaborator

@lucas-zimerman lucas-zimerman commented May 27, 2026

📜 Summary

  • Add captureRequestHeaders option to SentryGrpcInterceptor to attach outgoing gRPC metadata (request headers) to spans, with automatic PII filtering for sensitive headers (authorization, cookie, set-cookie, proxy-authorization)
  • Attach structured gRPC error details (ErrorInfo, BadRequest, RetryInfo, DebugInfo, PreconditionFailure, ResourceInfo, QuotaFailure) to spans on failed calls
  • Remove captureBody — no other Sentry SDK gRPC integration captures the request body
  • Improve example apps: typed DummyMessage class (replaces raw List<int> hand-encoding), WithHeaders button demonstrating custom gRPC metadata, SentryFlutter init in grpc4 example

💡 Motivation and Context

Brings the Dart gRPC interceptor closer to parity with the Python and JS integrations for request header propagation, while adding richer error detail capture that those SDKs don't have.

💚 How did you test it?

  • Ran both grpc4 and grpc5 example apps against grpcb.in
  • Verified spans appear in Sentry with http.request.header.* data when metadata is set
  • Verified error details are attached on failed calls
  • Pre-commit hooks (analyze + format) pass on all affected packages

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • No breaking changes

🔮 Next steps

lucas-zimerman and others added 7 commits May 20, 2026 18:23
Adds a new `sentry_grpc` package with `SentryGrpcInterceptor`, a
gRPC `ClientInterceptor` that provides:

- Distributed tracing via `sentry-trace` / `baggage` header injection
- Child span creation with `grpc.client` operation per unary call
- Breadcrumbs for outgoing RPC calls (method, status, duration)
- Optional exception capture for non-OK gRPC status codes
- Span-first lifecycle compatibility
- Support for grpc >=4.0.4 <6.0.0

Co-Authored-By: Claude <noreply@anthropic.com>
Each example is a Flutter app with three buttons:
- Good Request: GET rsa4096.badssl.com (valid TLS)
- Bad Request: GET expired.badssl.com (expired cert, captured by Sentry)
- gRPC Request: grpcb.in:9001 GRPCBin/Empty via SentryGrpcInterceptor

grpc4 resolves grpc 4.x; grpc5 resolves grpc 5.x. Both produce
identical behaviour to demonstrate version-agnostic compatibility.

Co-Authored-By: Claude <noreply@anthropic.com>
DummyUnary sends DummyMessage { f_string: "hello from sentry_grpc" }
and displays the echoed f_string from the response. The message is
hand-encoded via proto wire format (field 1, wire type 2) without
needing a protobuf codegen step.

RandomError triggers a random gRPC error from grpcb.in, which is
automatically captured by SentryGrpcInterceptor and reported to Sentry.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add captureRequestHeaders option to attach outgoing gRPC
  metadata to spans, with PII filtering for sensitive headers
- Attach gRPC error details (ErrorInfo, BadRequest, RetryInfo,
  DebugInfo, PreconditionFailure, ResourceInfo, QuotaFailure)
  to spans on failed calls
- Remove captureBody (not captured by other SDK integrations)
- Improve example apps: typed DummyMessage, WithHeaders button
  with custom metadata, SentryFlutter init in grpc4

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Features

  • (grpc) Add integration support for GRPC by lucas-zimerman in #3721

Fixes

Dart

  • Make sentryOnError synchronous in runZonedGuarded by theprantadutta in #3697
  • Route SDK diagnostic logs to browser console on web by theprantadutta in #3698

Flutter

  • Forward sample rate to native SDKs by buenaflor in #3722
  • Release Android JNI refs by buenaflor in #3712
  • Release replay JNI refs by buenaflor in #3699

Enhancements

Flutter

  • Move Android JNI work to core worker to avoid work on main isolate by buenaflor in #3713
  • Optimize Android scope sync by buenaflor in #3708

Dependencies

Deps

  • chore(deps): update Cocoa SDK to v8.58.3 by github-actions in #3726
  • chore(deps): update Android SDK to v8.43.0 by github-actions in #3727
  • chore(deps): update Android SDK to v8.42.0 by github-actions in #3715

Internal Changes

  • (feedback) Rename SentryFeedbackWidget to SentryFeedbackForm by denrase in #3702
  • (flutter) Align CI with stable SwiftPM defaults by buenaflor in #3710

🤖 This preview updates automatically when you update the PR.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.85%. Comparing base (b3b2719) to head (0623fa7).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3721      +/-   ##
==========================================
- Coverage   86.96%   86.85%   -0.11%     
==========================================
  Files         336      336              
  Lines       11982    11984       +2     
==========================================
- Hits        10420    10409      -11     
- Misses       1562     1575      +13     
Flag Coverage Δ
sentry 86.76% <ø> (ø)
sentry_dio 97.73% <ø> (ø)
sentry_drift 93.57% <ø> (ø)
sentry_file 65.29% <ø> (ø)
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 91.16% <ø> (-0.37%) ⬇️
sentry_hive 77.48% <ø> (ø)
sentry_isar 74.37% <ø> (ø)
sentry_link 21.50% <ø> (ø)
sentry_logging 97.01% <ø> (ø)
sentry_sqflite 88.81% <ø> (ø)
sentry_supabase 97.27% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@lucas-zimerman lucas-zimerman changed the title feat(grpc): Add headers and error details to spans feat(grpc): Add integration support for GRPC May 27, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

Android Performance metrics 🚀

  Plain With Sentry Diff
Startup time 362.04 ms 371.06 ms 9.02 ms
Size 14.55 MiB 15.87 MiB 1.31 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
20faa47 400.26 ms 392.90 ms -7.37 ms
6ad8fc4 489.92 ms 484.96 ms -4.96 ms
e8f0fc4 348.40 ms 353.34 ms 4.94 ms
57eccdf 437.96 ms 437.94 ms -0.02 ms
2e4277e 346.60 ms 358.29 ms 11.70 ms
e90cf93 374.35 ms 368.40 ms -5.95 ms
a920b0f 391.75 ms 378.98 ms -12.77 ms
55b7a00 418.43 ms 398.07 ms -20.35 ms
af96ef2 375.84 ms 382.02 ms 6.18 ms
1adf565 389.67 ms 395.96 ms 6.29 ms

App size

Revision Plain With Sentry Diff
20faa47 13.93 MiB 15.18 MiB 1.25 MiB
6ad8fc4 13.93 MiB 15.06 MiB 1.13 MiB
e8f0fc4 14.55 MiB 15.87 MiB 1.31 MiB
57eccdf 14.30 MiB 15.49 MiB 1.19 MiB
2e4277e 14.55 MiB 15.87 MiB 1.31 MiB
e90cf93 14.31 MiB 15.56 MiB 1.25 MiB
a920b0f 14.09 MiB 15.28 MiB 1.19 MiB
55b7a00 14.30 MiB 15.49 MiB 1.19 MiB
af96ef2 13.93 MiB 15.18 MiB 1.25 MiB
1adf565 14.09 MiB 15.28 MiB 1.19 MiB

Previous results on branch: lz/grpc

Startup times

Revision Plain With Sentry Diff
247fbe6 352.18 ms 336.59 ms -15.59 ms
a4d676d 354.69 ms 354.87 ms 0.18 ms
166b609 358.77 ms 357.23 ms -1.54 ms

App size

Revision Plain With Sentry Diff
247fbe6 14.55 MiB 15.87 MiB 1.31 MiB
a4d676d 14.55 MiB 15.87 MiB 1.31 MiB
166b609 14.55 MiB 15.87 MiB 1.31 MiB

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 27, 2026

iOS Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1243.60 ms 1245.25 ms 1.65 ms
Size 5.83 MiB 6.28 MiB 462.27 KiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6ba4675 1223.12 ms 1238.17 ms 15.04 ms
b3b2719 1257.07 ms 1264.57 ms 7.50 ms
e1ab497 1260.92 ms 1260.22 ms -0.69 ms
944b773 1252.82 ms 1254.08 ms 1.27 ms
765aa8b 1259.09 ms 1269.90 ms 10.82 ms
3fe9e80 1238.04 ms 1245.22 ms 7.18 ms
bfabaf2 1251.72 ms 1253.38 ms 1.67 ms
9d516f3 1256.04 ms 1262.70 ms 6.66 ms
8825ed8 1243.35 ms 1240.39 ms -2.96 ms
40c8f93 1234.27 ms 1261.98 ms 27.71 ms

App size

Revision Plain With Sentry Diff
6ba4675 7.86 MiB 9.44 MiB 1.58 MiB
b3b2719 5.83 MiB 6.28 MiB 462.27 KiB
e1ab497 5.53 MiB 6.01 MiB 487.96 KiB
944b773 5.53 MiB 6.00 MiB 479.98 KiB
765aa8b 7.86 MiB 9.44 MiB 1.58 MiB
3fe9e80 5.83 MiB 6.28 MiB 461.20 KiB
bfabaf2 5.53 MiB 6.01 MiB 487.95 KiB
9d516f3 5.73 MiB 6.18 MiB 463.54 KiB
8825ed8 5.53 MiB 5.97 MiB 453.71 KiB
40c8f93 5.53 MiB 6.00 MiB 479.94 KiB

Previous results on branch: lz/grpc

Startup times

Revision Plain With Sentry Diff
247fbe6 1248.98 ms 1251.13 ms 2.15 ms
166b609 1263.55 ms 1261.00 ms -2.55 ms
a4d676d 1256.67 ms 1256.33 ms -0.34 ms

App size

Revision Plain With Sentry Diff
247fbe6 5.83 MiB 6.28 MiB 460.84 KiB
166b609 5.83 MiB 6.28 MiB 462.27 KiB
a4d676d 5.83 MiB 6.28 MiB 462.27 KiB

Comment thread packages/grpc/lib/src/sentry_grpc_interceptor.dart Outdated
Comment thread packages/grpc/test/sentry_grpc_interceptor_test.dart
lucas-zimerman and others added 9 commits May 27, 2026 03:34
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Remove Flutter dependency, platform folders (linux/android/web),
and empty widget tests. Examples now run with `dart run lib/main.dart`.

Co-Authored-By: Claude <noreply@anthropic.com>
Flutter tool file, not needed for building or running.

Co-Authored-By: Claude <noreply@anthropic.com>
Comment thread packages/grpc/lib/src/sentry_grpc_interceptor.dart Outdated
Comment thread packages/grpc/lib/src/sentry_grpc_interceptor.dart
Comment thread packages/grpc/lib/src/sentry_grpc_interceptor.dart
Comment thread packages/grpc/test/mocks/no_such_method_provider.dart Outdated
Comment thread packages/grpc/lib/src/sentry_grpc_interceptor.dart Outdated
Copy link
Copy Markdown
Contributor

@buenaflor buenaflor left a comment

Choose a reason for hiding this comment

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

Did a quick PR review for now (didn't look into the details yet)

Comment thread packages/grpc/CHANGELOG.md Outdated
Comment on lines +6 to +26
class MockHub with NoSuchMethodProvider implements Hub {
List<CaptureExceptionCall> captureExceptionCalls = [];
List<AddBreadcrumbCall> addBreadcrumbCalls = [];
int closeCalls = 0;
bool _isEnabled = true;

@override
Scope get scope => Scope(_options);

final _options = defaultTestOptions();

@override
// ignore: invalid_use_of_internal_member
SentryOptions get options => _options;

void reset() {
captureExceptionCalls = [];
addBreadcrumbCalls = [];
closeCalls = 0;
_isEnabled = true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we already have a MockHub in the dart package, maybe it makes sense to move that mockhub from the dart package to the _sentry_testing package so we can use it easily across packages

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.

Is it ok if I do this task on a follow-up PR?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

sounds good

Comment on lines +243 to +270
span.setData('grpc.error_info.reason', detail.reason);
span.setData('grpc.error_info.domain', detail.domain);
if (detail.metadata.isNotEmpty) {
span.setData('grpc.error_info.metadata', detail.metadata.toString());
}
} else if (detail is rpc.BadRequest) {
span.setData(
'grpc.bad_request.field_violations',
detail.fieldViolations
.map((v) => '${v.field_1}: ${v.description}')
.join('; '),
);
} else if (detail is rpc.RetryInfo) {
span.setData(
'grpc.retry_info.retry_delay',
'${detail.retryDelay.seconds}s',
);
} else if (detail is rpc.DebugInfo) {
span.setData('grpc.debug_info.detail', detail.detail);
} else if (detail is rpc.PreconditionFailure) {
span.setData(
'grpc.precondition_failure.violations',
detail.violations
.map((v) => '${v.type}: ${v.subject} - ${v.description}')
.join('; '),
);
} else if (detail is rpc.ResourceInfo) {
span.setData('grpc.resource_info.type', detail.resourceType);
Copy link
Copy Markdown
Contributor

@buenaflor buenaflor May 27, 2026

Choose a reason for hiding this comment

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

Since we add a bunch of new attributes double check if OTEL has any equivalent attribute: https://opentelemetry.io/docs/specs/semconv/rpc/

if not, we should open a PR adding new ones in https://getsentry.github.io/sentry-conventions/

Let's double check also the python and go instrumentation to see how they structure their attributes and span ops

also let's add them to packages/dart/constants.dart in SemanticAttributesConstants

lucas-zimerman and others added 2 commits May 28, 2026 15:52
- Remove manual CHANGELOG entry (auto-generated from PR title)
- Add gRPC error detail attribute constants to SemanticAttributesConstants
- Use constants in interceptor instead of raw strings
- Wrap fire-and-forget response.then() with unawaited()
- Capture StackTrace in onError and pass to captureException
- Gate ErrorInfo.metadata behind sendDefaultPii
- Check tracePropagationTargets before injecting trace headers
- Fix noSuchMethod to throw UnsupportedError instead of silently dropping
- Add tests for _attachErrorDetails (ErrorInfo, BadRequest, RetryInfo,
  DebugInfo, ResourceInfo) including PII guard for metadata
- Add tests for tracePropagationTargets header injection control

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines +288 to +292
} else if (detail is rpc.DebugInfo) {
span.setData(
SemanticAttributesConstants.grpcDebugInfoDetail,
detail.detail,
);
Copy link
Copy Markdown

@sentry-warden sentry-warden Bot May 28, 2026

Choose a reason for hiding this comment

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

DebugInfo.detail attached to span without sendDefaultPii guard

Unlike ErrorInfo.metadata (which is gated on sendDefaultPii), DebugInfo.detail is always added to the span — this field typically contains server-side stack traces that could expose sensitive internals and should also require sendDefaultPii to be enabled.

Evidence
  • ErrorInfo.metadata is only attached when _hub.options.sendDefaultPii is true (line 270).
  • DebugInfo.detail at lines 288–292 has no such guard; it is always written to the span via span.setData.
  • The gRPC DebugInfo proto is explicitly designed to carry server stack traces (stack_entries) and free-form detail text, both potentially sensitive.
  • The PreconditionFailure, ResourceInfo, and QuotaFailure blocks also lack any PII gate, but those fields (type/subject/description) are lower risk than raw stack trace text.

Suggested fix: Wrap the DebugInfo setData call in a sendDefaultPii check, consistent with the ErrorInfo.metadata guard.

Suggested change
} else if (detail is rpc.DebugInfo) {
span.setData(
SemanticAttributesConstants.grpcDebugInfoDetail,
detail.detail,
);
if (_hub.options.sendDefaultPii) {
span.setData(
SemanticAttributesConstants.grpcDebugInfoDetail,
detail.detail,
);
}
Also found at 1 additional location
  • packages/grpc/lib/src/sentry_grpc_interceptor.dart:119

Identified by Warden code-review, find-bugs · J93-NEA

Comment on lines +186 to +187
CallOptions _buildModifiedOptions(
CallOptions options,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-standard credential headers bypass PII filter and are sent to Sentry by default

The _sensitiveHeaders allowlist only covers 4 headers (authorization, cookie, set-cookie, proxy-authorization). Common credential headers such as x-api-key, x-auth-token, x-access-token, and grpc-authorization pass through unconditionally when captureRequestHeaders is true (the default) and sendDefaultPii is false, leaking credentials into Sentry spans.

Evidence
  • _sensitiveHeaders is defined at lines 41-46 with exactly 4 entries: authorization, cookie, set-cookie, proxy-authorization.
  • _attachRequestData at line 186 skips a key only when !sendPii && _sensitiveHeaders.contains(normalizedKey) — every other key proceeds to span.setData at line 187.
  • captureRequestHeaders defaults to true (constructor line 69), so this runs for all users unless explicitly disabled.
  • sendDefaultPii defaults to false in SentryOptions, meaning no PII opt-in is needed to trigger the leak.
  • Headers like x-api-key are widely used for gRPC service authentication and would be sent to Sentry without any user awareness.

Identified by Warden find-bugs · 25T-W9K

Correct attribute keys per the OTEL gRPC semconv spec:
- rpc.system.name instead of rpc.system
- rpc.response.status_code (string) instead of rpc.grpc.status_code (int)
- rpc.request.metadata.<key> instead of http.request.header.*
- rpc.method now contains full service/method path (no rpc.service)

Co-Authored-By: Claude <noreply@anthropic.com>

/// Prefix for RPC request metadata. Append the lowercase metadata key.
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
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

/// The RPC system identifier. For gRPC, always `'grpc'`.
///
/// See https://opentelemetry.io/docs/specs/semconv/rpc/grpc/
static const rpcSystemName = 'rpc.system.name';
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 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';
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants