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:
|
| // 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.
| // OperationName overrides the default operation name (grpc.server). | ||
| OperationName string |
There was a problem hiding this comment.
Let's not expose this to users.
|
|
||
| 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.
| // OperationName overrides the default operation name (grpc.client). | ||
| OperationName string |
There was a problem hiding this comment.
Let's remove this here as well.
| // ReportOn defines the conditions under which errors are reported to Sentry. | ||
| ReportOn func(error) bool |
| 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.
sl0thentr0py
left a comment
There was a problem hiding this comment.
one comment about baggage
| // 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) { |
There was a problem hiding this comment.
i would put sentry stuff first since baggage has a size limit so while we respect third party, ours should take precedence
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: Data race on
stopMonitorfield in stream wrapper- Initialized stopMonitor to a no-op function before context.AfterFunc to prevent concurrent read/write access when the context is already done.
Preview (16fd93e883)
diff --git a/.craft.yml b/.craft.yml
--- a/.craft.yml
+++ b/.craft.yml
@@ -28,6 +28,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
@@ -47,7 +47,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 2262 linesYou can send follow-ups to the cloud agent here.
Initialize stopMonitor to a no-op function before calling context.AfterFunc to prevent a data race where the AfterFunc callback reads stopMonitor concurrently with the assignment if the context is already done. Co-Authored-By: Claude Sonnet 4.5 <claude-sonnet-4.5@cursor.sh>
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: Non-deterministic baggage member ordering breaks truncation guarantee
- Modified Baggage.String() to sort keys deterministically with sentry- prefixed members first, ensuring W3C Baggage truncation guarantees are met.
Preview (4f11aac865)
diff --git a/.craft.yml b/.craft.yml
--- a/.craft.yml
+++ b/.craft.yml
@@ -28,6 +28,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,50 @@
+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)
+ }
+
+ existingBaggage, 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
+ }
+
+ // Start with sentry baggage as the base to ensure sentry members
+ // appear first in the output. This is critical for W3C Baggage spec
+ // compliance: if size limits are exceeded, truncation happens from
+ // the end, so sentry's distributed tracing members must take precedence.
+ finalBaggage := sentryBaggage
+ for _, member := range existingBaggage.Members() {
+ // Only add third-party members if they don't conflict with sentry keys
+ if sentryBaggage.Member(member.Key()).Key() == "" {
+ 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,154 @@
+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)
+ }
+ })
+
+ t.Run("sentry members appear before third party members", func(t *testing.T) {
+ got, err := MergeBaggage("thirdparty=value", "sentry-trace_id=123")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ // Parse and verify member order - sentry members should come first
+ parsed, err := parseBaggageString(got)
+ if err != nil {
+ t.Fatalf("failed to parse result: %v", err)
+ }
+
+ // The first member should be a sentry- member
+ if len(parsed) < 2 {
+ t.Fatalf("expected at least 2 members, got %d", len(parsed))
+ }
+ if parsed[0].key != "sentry-trace_id" {
+ t.Errorf("expected first member to be sentry-trace_id, got %s", parsed[0].key)
+ }
+ })
+}
+
+type baggageMember struct {
+ key string
+ value string
+}
+
+func parseBaggageString(s string) ([]baggageMember, error) {
+ if s == "" {
... diff truncated: showing 800 of 2541 linesYou can send follow-ups to the cloud agent here.
4f11aac to
026f197
Compare
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: Duplicated span status logic across client and server
- Refactored finishSpan to call setRPCStatus internally, eliminating the duplication of span status-setting logic.
Preview (9fd99bf004)
diff --git a/.craft.yml b/.craft.yml
--- a/.craft.yml
+++ b/.craft.yml
@@ -28,6 +28,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,52 @@
+package sentry
+
+import (
+ "fmt"
+ "strings"
+
+ "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)
+ }
+
+ existingBaggage, 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
+ }
+
+ sentryKeys := make(map[string]struct{}, sentryBaggage.Len())
+ for _, member := range sentryBaggage.Members() {
+ sentryKeys[member.Key()] = struct{}{}
+ }
+
+ parts := make([]string, 0, sentryBaggage.Len()+existingBaggage.Len())
+ if s := sentryBaggage.String(); s != "" {
+ parts = append(parts, s)
+ }
+ for _, member := range existingBaggage.Members() {
+ if _, collides := sentryKeys[member.Key()]; collides {
+ continue
+ }
+ parts = append(parts, member.String())
+ }
+
+ return strings.Join(parts, ","), 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
@@ -47,7 +47,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 truncated: showing 800 of 2273 linesYou can send follow-ups to the cloud agent here.
Refactored finishSpan to call setRPCStatus internally instead of duplicating the same three lines (grpcStatusCode, toSpanStatus, SetData). This avoids maintaining identical status-setting logic in two places and ensures consistency if the logic needs to change. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
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: WaitForDelivery flushes every response unlike other integrations
- Moved hub.Flush() from unconditional defer to inside recoverWithSentry, gated by eventID != nil check, aligning with other integrations to only flush on panic recovery.
Preview (43747179dc)
diff --git a/.craft.yml b/.craft.yml
--- a/.craft.yml
+++ b/.craft.yml
@@ -28,6 +28,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,52 @@
+package sentry
+
+import (
+ "fmt"
+ "strings"
+
+ "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)
+ }
+
+ existingBaggage, 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
+ }
+
+ sentryKeys := make(map[string]struct{}, sentryBaggage.Len())
+ for _, member := range sentryBaggage.Members() {
+ sentryKeys[member.Key()] = struct{}{}
+ }
+
+ parts := make([]string, 0, sentryBaggage.Len()+existingBaggage.Len())
+ if s := sentryBaggage.String(); s != "" {
+ parts = append(parts, s)
+ }
+ for _, member := range existingBaggage.Members() {
+ if _, collides := sentryKeys[member.Key()]; collides {
+ continue
+ }
+ parts = append(parts, member.String())
+ }
+
+ return strings.Join(parts, ","), 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
@@ -47,7 +47,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 truncated: showing 800 of 2391 linesYou can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 404bfe1. Configure here.

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