Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #938 +/- ##
==========================================
+ Coverage 84.00% 84.32% +0.31%
==========================================
Files 50 52 +2
Lines 5171 5340 +169
==========================================
+ Hits 4344 4503 +159
- Misses 673 680 +7
- Partials 154 157 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
grpc/server.go
Outdated
| // CaptureRequestBody determines whether to capture and send request bodies to Sentry. | ||
| CaptureRequestBody bool |
There was a problem hiding this comment.
This should be handled by the base SDK.
grpc/server.go
Outdated
| // OperationName overrides the default operation name (grpc.server). | ||
| OperationName string |
There was a problem hiding this comment.
Let's not expose this to users.
grpc/server.go
Outdated
|
|
||
| options := []sentry.SpanOption{ | ||
| sentry.ContinueTrace(hub, sentryTraceHeader, sentryBaggageHeader), | ||
| sentry.WithOpName(opts.OperationName), |
There was a problem hiding this comment.
Let's hard code this grpc.server.
| examplepb.RegisterExampleServiceServer(server, &ExampleServiceServer{}) | ||
|
|
||
| // Start the server | ||
| listener, err := net.Listen("tcp", grpcPort) |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
Detected a network listener listening on 0.0.0.0 or an empty string. This could unexpectedly expose the server publicly as it binds to all available interfaces. Instead, specify another IP address that is not 0.0.0.0 nor the empty string.
To resolve this comment:
✨ Commit Assistant Fix Suggestion
- Update the value of
grpcPortso it is not just a port or set to0.0.0.0.
For example, ifgrpcPortis":50051", change it to"127.0.0.1:50051"or another appropriate interface (like your private network IP address). - If you need the server to be accessible only from the local machine, use
"127.0.0.1:<port>"as the address when callingnet.Listen. - If you do need remote access, restrict the IP address as much as possible to only the needed network interface, rather than using
0.0.0.0or a blank string. - Example:
listener, err := net.Listen("tcp", "127.0.0.1:50051")
When a server binds to 0.0.0.0 or just the port (like ":50051"), it listens on all interfaces, which could make your service accessible from unwanted sources. Use a specific IP to limit access.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by avoid-bind-to-all-interfaces.
You can view more details about this finding in the Semgrep AppSec Platform.
grpc/client.go
Outdated
| // OperationName overrides the default operation name (grpc.client). | ||
| OperationName string |
There was a problem hiding this comment.
Let's remove this here as well.
grpc/server.go
Outdated
| // ReportOn defines the conditions under which errors are reported to Sentry. | ||
| ReportOn func(error) bool |
grpc/server.go
Outdated
| func reportErrorToSentry(hub *sentry.Hub, err error, methodName string, req any, md map[string]string) { | ||
| hub.WithScope(func(scope *sentry.Scope) { | ||
| scope.SetExtras(map[string]any{ | ||
| "grpc.method": methodName, | ||
| "grpc.error": err.Error(), | ||
| }) | ||
|
|
||
| if req != nil { | ||
| scope.SetExtra("request", req) | ||
| } | ||
|
|
||
| if len(md) > 0 { | ||
| scope.SetExtra("metadata", md) | ||
| } | ||
|
|
||
| defer hub.CaptureException(err) | ||
|
|
||
| statusErr, ok := status.FromError(err) | ||
| if !ok { | ||
| return | ||
| } | ||
|
|
||
| for _, detail := range statusErr.Details() { | ||
| debugInfo, ok := detail.(*errdetails.DebugInfo) | ||
| if !ok { | ||
| continue | ||
| } | ||
| hub.AddBreadcrumb(&sentry.Breadcrumb{ | ||
| Type: "debug", | ||
| Category: "grpc.server", | ||
| Message: debugInfo.Detail, | ||
| Data: map[string]any{"stackTrace": strings.Join(debugInfo.StackEntries, "\n")}, | ||
| Level: sentry.LevelError, | ||
| Timestamp: time.Now(), | ||
| }, nil) | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
The reported errors here add zero value. Let's remove this.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Default timeout inconsistent with all other integrations
- Changed gRPC server interceptor default timeout from 5s to 2s to match all other HTTP integrations in the project.
You can send follow-ups to this agent here.
Changed gRPC server interceptor default timeout from 5s to 2s to match the default timeout used by all other HTTP integrations (echo, gin, fiber, fasthttp, http, iris, negroni). Co-Authored-By: Claude Sonnet 4.5 <noreply@example.com>
8b87c36 to
e19e1ad
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed:
WaitForDeliveryoption ineffective for non-panic requests- Added hub.Flush() calls after both unary and streaming handlers complete when WaitForDelivery is true, ensuring events captured during normal request handling are delivered before the response is sent.
Preview (970151ee5c)
diff --git a/.craft.yml b/.craft.yml
--- a/.craft.yml
+++ b/.craft.yml
@@ -25,6 +25,9 @@
tagPrefix: gin/v
tagOnly: true
- name: github
+ tagPrefix: grpc/v
+ tagOnly: true
+ - name: github
tagPrefix: iris/v
tagOnly: true
- name: github
diff --git a/_examples/grpc/client/main.go b/_examples/grpc/client/main.go
new file mode 100644
--- /dev/null
+++ b/_examples/grpc/client/main.go
@@ -1,0 +1,119 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "time"
+
+ "grpcdemo/cmd/server/examplepb"
+
+ "github.com/getsentry/sentry-go"
+ sentrygrpc "github.com/getsentry/sentry-go/grpc"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/metadata"
+)
+
+const grpcServerAddress = "localhost:50051"
+
+func main() {
+ // Initialize Sentry
+ err := sentry.Init(sentry.ClientOptions{
+ Dsn: "",
+ TracesSampleRate: 1.0,
+ })
+ if err != nil {
+ log.Fatalf("sentry.Init: %s", err)
+ }
+ defer sentry.Flush(2 * time.Second)
+
+ // Create a connection to the gRPC server with Sentry interceptors
+ conn, err := grpc.NewClient(
+ grpcServerAddress,
+ grpc.WithTransportCredentials(insecure.NewCredentials()), // Use TLS in production
+ grpc.WithUnaryInterceptor(sentrygrpc.UnaryClientInterceptor()),
+ grpc.WithStreamInterceptor(sentrygrpc.StreamClientInterceptor()),
+ )
+ if err != nil {
+ log.Fatalf("Failed to connect to gRPC server: %s", err)
+ }
+ defer conn.Close()
+
+ // Create a client for the ExampleService
+ client := examplepb.NewExampleServiceClient(conn)
+
+ // Perform Unary call
+ fmt.Println("Performing Unary Call:")
+ unaryExample(client)
+
+ // Perform Streaming call
+ fmt.Println("\nPerforming Streaming Call:")
+ streamExample(client)
+}
+
+func unaryExample(client examplepb.ExampleServiceClient) {
+ ctx := context.Background()
+
+ // Add metadata to the context
+ ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
+ "custom-header", "value",
+ ))
+
+ req := &examplepb.ExampleRequest{
+ Message: "Hello, server!", // Change to "error" to simulate an error
+ }
+
+ res, err := client.UnaryExample(ctx, req)
+ if err != nil {
+ fmt.Printf("Unary Call Error: %v\n", err)
+ sentry.CaptureException(err)
+ return
+ }
+
+ fmt.Printf("Unary Response: %s\n", res.Message)
+}
+
+func streamExample(client examplepb.ExampleServiceClient) {
+ ctx := context.Background()
+
+ // Add metadata to the context
+ ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(
+ "streaming-header", "stream-value",
+ ))
+
+ stream, err := client.StreamExample(ctx)
+ if err != nil {
+ fmt.Printf("Failed to establish stream: %v\n", err)
+ sentry.CaptureException(err)
+ return
+ }
+
+ // Send multiple messages in the stream
+ messages := []string{"Message 1", "Message 2", "error", "Message 4"}
+ for _, msg := range messages {
+ err := stream.Send(&examplepb.ExampleRequest{Message: msg})
+ if err != nil {
+ fmt.Printf("Stream Send Error: %v\n", err)
+ sentry.CaptureException(err)
+ return
+ }
+ }
+
+ // Close the stream for sending
+ stream.CloseSend()
+
+ // Receive responses from the server
+ for {
+ res, err := stream.Recv()
+ if err != nil {
+ if err != io.EOF {
+ fmt.Printf("Stream Recv Error: %v\n", err)
+ sentry.CaptureException(err)
+ }
+ break
+ }
+ fmt.Printf("Stream Response: %s\n", res.Message)
+ }
+}
diff --git a/_examples/grpc/server/example.proto b/_examples/grpc/server/example.proto
new file mode 100644
--- /dev/null
+++ b/_examples/grpc/server/example.proto
@@ -1,0 +1,21 @@
+syntax = "proto3";
+
+package main;
+
+option go_package = "github.com/your-username/your-repo/examplepb;examplepb";
+
+// ExampleService defines the gRPC service.
+service ExampleService {
+ rpc UnaryExample(ExampleRequest) returns (ExampleResponse);
+ rpc StreamExample(stream ExampleRequest) returns (stream ExampleResponse);
+}
+
+// ExampleRequest is the request message.
+message ExampleRequest {
+ string message = 1;
+}
+
+// ExampleResponse is the response message.
+message ExampleResponse {
+ string message = 1;
+}
diff --git a/_examples/grpc/server/examplepb/example.pb.go b/_examples/grpc/server/examplepb/example.pb.go
new file mode 100644
--- /dev/null
+++ b/_examples/grpc/server/examplepb/example.pb.go
@@ -1,0 +1,191 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.1
+// protoc v5.29.2
+// source: example.proto
+
+package examplepb
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// ExampleRequest is the request message.
+type ExampleRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExampleRequest) Reset() {
+ *x = ExampleRequest{}
+ mi := &file_example_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExampleRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExampleRequest) ProtoMessage() {}
+
+func (x *ExampleRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_example_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExampleRequest.ProtoReflect.Descriptor instead.
+func (*ExampleRequest) Descriptor() ([]byte, []int) {
+ return file_example_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *ExampleRequest) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+// ExampleResponse is the response message.
+type ExampleResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ExampleResponse) Reset() {
+ *x = ExampleResponse{}
+ mi := &file_example_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ExampleResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ExampleResponse) ProtoMessage() {}
+
+func (x *ExampleResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_example_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ExampleResponse.ProtoReflect.Descriptor instead.
+func (*ExampleResponse) Descriptor() ([]byte, []int) {
+ return file_example_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *ExampleResponse) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+var File_example_proto protoreflect.FileDescriptor
+
+var file_example_proto_rawDesc = []byte{
+ 0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+ 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x22, 0x2a, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
+ 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
+ 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x8f,
+ 0x01, 0x0a, 0x0e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
+ 0x65, 0x12, 0x3b, 0x0a, 0x0c, 0x55, 0x6e, 0x61, 0x72, 0x79, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c,
+ 0x65, 0x12, 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45,
+ 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40,
+ 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12,
+ 0x14, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x45, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01,
+ 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79,
+ 0x6f, 0x75, 0x72, 0x2d, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x2f, 0x79, 0x6f, 0x75,
+ 0x72, 0x2d, 0x72, 0x65, 0x70, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62,
+ 0x3b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x33,
+}
+
+var (
+ file_example_proto_rawDescOnce sync.Once
+ file_example_proto_rawDescData = file_example_proto_rawDesc
+)
+
+func file_example_proto_rawDescGZIP() []byte {
+ file_example_proto_rawDescOnce.Do(func() {
+ file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData)
+ })
+ return file_example_proto_rawDescData
+}
+
+var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_example_proto_goTypes = []any{
+ (*ExampleRequest)(nil), // 0: main.ExampleRequest
+ (*ExampleResponse)(nil), // 1: main.ExampleResponse
+}
+var file_example_proto_depIdxs = []int32{
+ 0, // 0: main.ExampleService.UnaryExample:input_type -> main.ExampleRequest
+ 0, // 1: main.ExampleService.StreamExample:input_type -> main.ExampleRequest
+ 1, // 2: main.ExampleService.UnaryExample:output_type -> main.ExampleResponse
+ 1, // 3: main.ExampleService.StreamExample:output_type -> main.ExampleResponse
+ 2, // [2:4] is the sub-list for method output_type
+ 0, // [0:2] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_example_proto_init() }
+func file_example_proto_init() {
+ if File_example_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_example_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_example_proto_goTypes,
+ DependencyIndexes: file_example_proto_depIdxs,
+ MessageInfos: file_example_proto_msgTypes,
+ }.Build()
+ File_example_proto = out.File
+ file_example_proto_rawDesc = nil
+ file_example_proto_goTypes = nil
+ file_example_proto_depIdxs = nil
+}
diff --git a/_examples/grpc/server/examplepb/example_grpc.pb.go b/_examples/grpc/server/examplepb/example_grpc.pb.go
new file mode 100644
--- /dev/null
+++ b/_examples/grpc/server/examplepb/example_grpc.pb.go
@@ -1,0 +1,158 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc v5.29.2
+// source: example.proto
+
+package examplepb
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ ExampleService_UnaryExample_FullMethodName = "/main.ExampleService/UnaryExample"
+ ExampleService_StreamExample_FullMethodName = "/main.ExampleService/StreamExample"
+)
+
+// ExampleServiceClient is the client API for ExampleService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// ExampleService defines the gRPC service.
+type ExampleServiceClient interface {
+ UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error)
+ StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error)
+}
+
+type exampleServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewExampleServiceClient(cc grpc.ClientConnInterface) ExampleServiceClient {
+ return &exampleServiceClient{cc}
+}
+
+func (c *exampleServiceClient) UnaryExample(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ExampleResponse)
+ err := c.cc.Invoke(ctx, ExampleService_UnaryExample_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *exampleServiceClient) StreamExample(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[ExampleRequest, ExampleResponse], error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ stream, err := c.cc.NewStream(ctx, &ExampleService_ServiceDesc.Streams[0], ExampleService_StreamExample_FullMethodName, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ x := &grpc.GenericClientStream[ExampleRequest, ExampleResponse]{ClientStream: stream}
+ return x, nil
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type ExampleService_StreamExampleClient = grpc.BidiStreamingClient[ExampleRequest, ExampleResponse]
+
+// ExampleServiceServer is the server API for ExampleService service.
+// All implementations must embed UnimplementedExampleServiceServer
+// for forward compatibility.
+//
+// ExampleService defines the gRPC service.
+type ExampleServiceServer interface {
+ UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error)
+ StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error
+ mustEmbedUnimplementedExampleServiceServer()
+}
+
+// UnimplementedExampleServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedExampleServiceServer struct{}
+
+func (UnimplementedExampleServiceServer) UnaryExample(context.Context, *ExampleRequest) (*ExampleResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method UnaryExample not implemented")
+}
+func (UnimplementedExampleServiceServer) StreamExample(grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]) error {
+ return status.Errorf(codes.Unimplemented, "method StreamExample not implemented")
+}
+func (UnimplementedExampleServiceServer) mustEmbedUnimplementedExampleServiceServer() {}
+func (UnimplementedExampleServiceServer) testEmbeddedByValue() {}
+
+// UnsafeExampleServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ExampleServiceServer will
+// result in compilation errors.
+type UnsafeExampleServiceServer interface {
+ mustEmbedUnimplementedExampleServiceServer()
+}
+
+func RegisterExampleServiceServer(s grpc.ServiceRegistrar, srv ExampleServiceServer) {
+ // If the following call pancis, it indicates UnimplementedExampleServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&ExampleService_ServiceDesc, srv)
+}
+
+func _ExampleService_UnaryExample_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ExampleRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ExampleServiceServer).UnaryExample(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ExampleService_UnaryExample_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ExampleServiceServer).UnaryExample(ctx, req.(*ExampleRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _ExampleService_StreamExample_Handler(srv interface{}, stream grpc.ServerStream) error {
+ return srv.(ExampleServiceServer).StreamExample(&grpc.GenericServerStream[ExampleRequest, ExampleResponse]{ServerStream: stream})
+}
+
+// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
+type ExampleService_StreamExampleServer = grpc.BidiStreamingServer[ExampleRequest, ExampleResponse]
+
+// ExampleService_ServiceDesc is the grpc.ServiceDesc for ExampleService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ExampleService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "main.ExampleService",
+ HandlerType: (*ExampleServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "UnaryExample",
+ Handler: _ExampleService_UnaryExample_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{
+ {
+ StreamName: "StreamExample",
+ Handler: _ExampleService_StreamExample_Handler,
+ ServerStreams: true,
+ ClientStreams: true,
+ },
+ },
+ Metadata: "example.proto",
+}
diff --git a/_examples/grpc/server/main.go b/_examples/grpc/server/main.go
new file mode 100644
--- /dev/null
+++ b/_examples/grpc/server/main.go
@@ -1,0 +1,94 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net"
+ "time"
+
+ "github.com/getsentry/sentry-go"
+ "github.com/getsentry/sentry-go/_examples/grpc/server/examplepb"
+ sentrygrpc "github.com/getsentry/sentry-go/grpc"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/metadata"
+)
+
+const grpcPort = ":50051"
+
+// ExampleServiceServer is the server implementation for the ExampleService.
+type ExampleServiceServer struct {
+ examplepb.UnimplementedExampleServiceServer
+}
+
+// UnaryExample handles unary gRPC requests.
+func (s *ExampleServiceServer) UnaryExample(ctx context.Context, req *examplepb.ExampleRequest) (*examplepb.ExampleResponse, error) {
+ md, _ := metadata.FromIncomingContext(ctx)
+ fmt.Printf("Received Unary Request: %v\nMetadata: %v\n", req.Message, md)
+
+ // Simulate an error for demonstration
+ if req.Message == "error" {
+ return nil, fmt.Errorf("simulated unary error")
+ }
+
+ return &examplepb.ExampleResponse{Message: fmt.Sprintf("Hello, %s!", req.Message)}, nil
+}
+
+// StreamExample handles bidirectional streaming gRPC requests.
+func (s *ExampleServiceServer) StreamExample(stream examplepb.ExampleService_StreamExampleServer) error {
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ fmt.Printf("Stream Recv Error: %v\n", err)
+ return err
+ }
+
+ fmt.Printf("Received Stream Message: %v\n", req.Message)
+
+ if req.Message == "error" {
+ return fmt.Errorf("simulated stream error")
+ }
+
+ err = stream.Send(&examplepb.ExampleResponse{Message: fmt.Sprintf("Echo: %s", req.Message)})
+ if err != nil {
+ fmt.Printf("Stream Send Error: %v\n", err)
+ return err
+ }
+ }
+}
+
+func main() {
+ // Initialize Sentry
+ err := sentry.Init(sentry.ClientOptions{
+ Dsn: "",
+ TracesSampleRate: 1.0,
+ })
+ if err != nil {
+ log.Fatalf("sentry.Init: %s", err)
+ }
+ defer sentry.Flush(2 * time.Second)
+
+ // Create a new gRPC server with Sentry interceptors
+ server := grpc.NewServer(
+ grpc.UnaryInterceptor(sentrygrpc.UnaryServerInterceptor(sentrygrpc.ServerOptions{
+ Repanic: true,
+ })),
+ grpc.StreamInterceptor(sentrygrpc.StreamServerInterceptor(sentrygrpc.ServerOptions{
+ Repanic: true,
+ })),
+ )
+
+ // Register the ExampleService
+ examplepb.RegisterExampleServiceServer(server, &ExampleServiceServer{})
+
+ // Start the server
+ listener, err := net.Listen("tcp", grpcPort)
+ if err != nil {
+ log.Fatalf("Failed to listen on port %s: %v", grpcPort, err)
+ }
+
+ fmt.Printf("gRPC server is running on %s\n", grpcPort)
+ if err := server.Serve(listener); err != nil {
+ log.Fatalf("Failed to serve: %v", err)
+ }
+}
diff --git a/baggage.go b/baggage.go
new file mode 100644
--- /dev/null
+++ b/baggage.go
@@ -1,0 +1,42 @@
+package sentry
+
+import (
+ "fmt"
+
+ "github.com/getsentry/sentry-go/internal/debuglog"
+ "github.com/getsentry/sentry-go/internal/otel/baggage"
+)
+
+// MergeBaggage merges an existing baggage header with a Sentry-generated one.
+//
+// Existing third-party members are preserved. If both baggage strings contain
+// the same member key, the Sentry-generated member wins. The helper is best-effort
+// and only keeps the sentry baggage in case the existing one is malformed.
+func MergeBaggage(existingHeader, sentryHeader string) (string, error) {
+ // TODO: we are reparsing the headers here, because we currently don't
+ // expose a method to get only DSC or its baggage members.
+ sentryBaggage, err := baggage.Parse(sentryHeader)
+ if err != nil {
+ return "", fmt.Errorf("cannot parse sentryHeader: %w", err)
+ }
+
+ finalBaggage, err := baggage.Parse(existingHeader)
+ if err != nil {
+ if sentryBaggage.Len() == 0 {
+ return "", fmt.Errorf("cannot parse existingHeader: %w", err)
+ }
+ // in case that the incoming header is malformed we should only
+ // care about merging sentry related baggage information for distributed tracing.
+ debuglog.Printf("malformed incoming header: %v", err)
+ return sentryBaggage.String(), nil
+ }
+
+ for _, member := range sentryBaggage.Members() {
+ finalBaggage, err = finalBaggage.SetMember(member)
+ if err != nil {
+ return "", fmt.Errorf("cannot merge baggage: %w", err)
+ }
+ }
+
+ return finalBaggage.String(), nil
+}
diff --git a/baggage_test.go b/baggage_test.go
new file mode 100644
--- /dev/null
+++ b/baggage_test.go
@@ -1,0 +1,83 @@
+package sentry
+
+import "testing"
+
+func TestMergeBaggage(t *testing.T) {
+ t.Run("both empty", func(t *testing.T) {
+ got, err := MergeBaggage("", "")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if got != "" {
+ t.Fatalf("expected empty baggage, got %q", got)
+ }
+ })
+
+ t.Run("empty existing returns sentry baggage", func(t *testing.T) {
+ got, err := MergeBaggage("", "sentry-trace_id=123,sentry-sampled=true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ assertBaggageStringsEqual(t, got, "sentry-trace_id=123,sentry-sampled=true")
+ })
+
+ t.Run("empty sentry returns existing baggage", func(t *testing.T) {
+ got, err := MergeBaggage("othervendor=bla", "")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ assertBaggageStringsEqual(t, got, "othervendor=bla")
+ })
+
+ t.Run("preserves third party members", func(t *testing.T) {
+ got, err := MergeBaggage("othervendor=bla", "sentry-trace_id=123,sentry-sampled=true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ assertBaggageStringsEqual(t, got, "othervendor=bla,sentry-trace_id=123,sentry-sampled=true")
+ })
+
+ t.Run("sentry members override existing members", func(t *testing.T) {
+ got, err := MergeBaggage(
+ "othervendor=bla,sentry-trace_id=old,sentry-sampled=false",
+ "sentry-trace_id=new,sentry-sampled=true",
+ )
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ assertBaggageStringsEqual(t, got, "othervendor=bla,sentry-trace_id=new,sentry-sampled=true")
+ })
+
+ t.Run("invalid existing returns sentry baggage", func(t *testing.T) {
+ got, err := MergeBaggage("not-valid", "sentry-trace_id=123,sentry-sampled=true")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ assertBaggageStringsEqual(t, got, "sentry-trace_id=123,sentry-sampled=true")
+ })
+
+ t.Run("invalid sentry returns empty and error", func(t *testing.T) {
+ got, err := MergeBaggage("othervendor=bla", "sentry-trace_id=123,invalid member,sentry-sampled=true")
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if got != "" {
+ t.Fatalf("expected empty baggage, got %q", got)
+ }
+ })
+
+ t.Run("invalid existing with empty sentry still errors", func(t *testing.T) {
+ got, err := MergeBaggage("not-valid", "")
+ if err == nil {
+ t.Fatal("expected error")
+ }
+ if got != "" {
+ t.Fatalf("expected empty baggage, got %q", got)
+ }
+ })
+}
diff --git a/echo/sentryecho.go b/echo/sentryecho.go
--- a/echo/sentryecho.go
+++ b/echo/sentryecho.go
@@ -46,7 +46,7 @@
// It can be used with Use() methods.
func New(options Options) echo.MiddlewareFunc {
if options.Timeout == 0 {
- options.Timeout = 2 * time.Second
+ options.Timeout = sentry.DefaultFlushTimeout
}
return (&handler{
diff --git a/fasthttp/sentryfasthttp.go b/fasthttp/sentryfasthttp.go
--- a/fasthttp/sentryfasthttp.go
+++ b/fasthttp/sentryfasthttp.go
@@ -47,7 +47,7 @@
// that satisfy fasthttp.RequestHandler interface.
func New(options Options) *Handler {
if options.Timeout == 0 {
- options.Timeout = 2 * time.Second
+ options.Timeout = sentry.DefaultFlushTimeout
}
return &Handler{
diff --git a/fiber/sentryfiber.go b/fiber/sentryfiber.go
--- a/fiber/sentryfiber.go
+++ b/fiber/sentryfiber.go
@@ -48,7 +48,7 @@
// New returns a handler struct which satisfies Fiber's middleware interface
func New(options Options) fiber.Handler {
if options.Timeout == 0 {
- options.Timeout = 2 * time.Second
+ options.Timeout = sentry.DefaultFlushTimeout
}
... diff truncated: showing 800 of 2301 linesYou can send follow-ups to this agent here.
The WaitForDelivery option was only triggering a flush during panic recovery, but not after normal request handling. This meant events captured inside gRPC handlers (e.g., hub.CaptureException) would not be delivered before the response was sent, potentially leading to lost events if the process shut down shortly after. This change adds hub.Flush() calls after both unary and streaming handlers complete when WaitForDelivery is true, ensuring all events are delivered before the response is sent. Co-Authored-By: Claude Sonnet 4.5 <noreply@example.com>

Closes #240
Sample error logged to Sentry:
Possible improvements / todos:
sentryandsentry-docs