Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
8a50583
add ring buffer implementation
giortzisg Sep 24, 2025
0c154b8
add new transport
giortzisg Sep 24, 2025
2ccb148
fix race
giortzisg Sep 24, 2025
de5b70a
fix lint
giortzisg Sep 24, 2025
fc93d2c
fix lint
giortzisg Sep 25, 2025
3c7498e
fix tests
giortzisg Sep 25, 2025
0a406ba
modify envelope serialization and tests
giortzisg Sep 26, 2025
6f9c638
change transport opts
giortzisg Sep 26, 2025
bf26d59
remove transport.Configure
giortzisg Sep 26, 2025
c7205ca
add proper await on queue flush
giortzisg Sep 26, 2025
8c8a4bd
add test for marshall fallback
giortzisg Sep 29, 2025
4228142
fix tests
giortzisg Sep 29, 2025
803349d
add sendEvent
giortzisg Sep 29, 2025
51f0373
fix dsn exporting
giortzisg Sep 30, 2025
58a7b02
enhance transport test suite
giortzisg Sep 30, 2025
1ed184b
change backpressure test
giortzisg Sep 30, 2025
2d66573
merge categories
giortzisg Oct 6, 2025
7a2f452
Merge branch 'master' into feat/transport-envelope
giortzisg Oct 6, 2025
a146a1e
specify min tls version
giortzisg Oct 6, 2025
871ade0
use global debuglog
giortzisg Oct 6, 2025
4c15f3a
chore: amend debug output and func comments
giortzisg Oct 8, 2025
b2c7dc4
chore: refactor tests
giortzisg Oct 8, 2025
9d368d4
chore: fix overflow test
giortzisg Oct 8, 2025
d2b2a6f
chore: move overflow policy
giortzisg Oct 9, 2025
a357417
add noopTransport
giortzisg Oct 9, 2025
992feaf
add internalTransport wrapper
giortzisg Oct 9, 2025
bc5d4b8
chore: make internalTransportAdapter private
giortzisg Oct 9, 2025
f89d05d
chore: fix race
giortzisg Oct 9, 2025
ec10979
chore: reexport internal wrapper
giortzisg Oct 9, 2025
17d1a15
change RequestHeaders receiver
giortzisg Oct 9, 2025
d206afe
modify internal transport adapters
giortzisg Oct 9, 2025
06fd3f8
add batchsize & timeout to buffers
giortzisg Oct 9, 2025
193c8ad
Merge branch 'master' into feat/transport-buffers
giortzisg Oct 10, 2025
87ce064
Merge branch 'origin/feat/transport-buffers'
giortzisg Oct 10, 2025
3f2b23c
Merge branch 'origin/feat/transport-envelope'
giortzisg Oct 10, 2025
2b718b0
feat: add scheduler
giortzisg Oct 10, 2025
95106d8
Merge branch 'feat/transport-envelope' into feat/telemetry-scheduler
lcian Oct 10, 2025
d61df59
Merge branch 'feat/transport-buffers' into feat/transport-envelope
giortzisg Oct 10, 2025
6be99a3
Merge branch 'feat/transport-envelope' into feat/telemetry-scheduler
giortzisg Oct 10, 2025
e18a54d
properly batch logs
giortzisg Oct 10, 2025
37e1bad
chore: add debug statements for transport flush
giortzisg Oct 13, 2025
9ae389d
chore: fix buffer double flush
giortzisg Oct 13, 2025
c654832
fix: correctly batch log items
giortzisg Oct 13, 2025
ceef544
add debug log if buffer missing
giortzisg Oct 13, 2025
b8d15d6
fix client
giortzisg Oct 13, 2025
43b7812
add buffer interface
giortzisg Oct 20, 2025
7c87bb6
chore: rename logItem
giortzisg Oct 21, 2025
b1bfe59
chore: fix lint
giortzisg Oct 21, 2025
e0478a2
chore: fix processItems
giortzisg Oct 21, 2025
ea10ecb
Merge branch 'master' into feat/telemetry-scheduler
giortzisg Oct 22, 2025
cda84db
add buffer wrapper
giortzisg Oct 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ coverage:
threshold: 0.5%
ignore:
- "log_fallback.go"
- "internal/testutils"
69 changes: 63 additions & 6 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import (

"github.com/getsentry/sentry-go/internal/debug"
"github.com/getsentry/sentry-go/internal/debuglog"
httpInternal "github.com/getsentry/sentry-go/internal/http"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
"github.com/getsentry/sentry-go/internal/telemetry"
)

// The identifier of the SDK.
Expand Down Expand Up @@ -249,6 +253,8 @@ type ClientOptions struct {
//
// By default, this is empty and all status codes are traced.
TraceIgnoreStatusCodes [][]int
// EnableTelemetryBuffer enables the telemetry buffer layer for prioritized delivery of events.
EnableTelemetryBuffer bool
}

// Client is the underlying processor that is used by the main API and Hub
Expand All @@ -263,8 +269,9 @@ type Client struct {
sdkVersion string
// Transport is read-only. Replacing the transport of an existing client is
// not supported, create a new client instead.
Transport Transport
batchLogger *BatchLogger
Transport Transport
batchLogger *BatchLogger
telemetryBuffer *telemetry.Buffer
}

// NewClient creates and returns an instance of Client configured using
Expand Down Expand Up @@ -364,12 +371,15 @@ func NewClient(options ClientOptions) (*Client, error) {
sdkVersion: SDKVersion,
}

if options.EnableLogs {
client.setupTransport()

if options.EnableTelemetryBuffer {
client.setupTelemetryBuffer()
} else if options.EnableLogs {
client.batchLogger = NewBatchLogger(&client)
client.batchLogger.Start()
}

client.setupTransport()
client.setupIntegrations()

return &client, nil
Expand All @@ -391,6 +401,41 @@ func (client *Client) setupTransport() {
client.Transport = transport
}

func (client *Client) setupTelemetryBuffer() {
if !client.options.EnableTelemetryBuffer {
return
}

if client.dsn == nil {
debuglog.Println("Telemetry buffer disabled: no DSN configured")
return
}

transport := httpInternal.NewAsyncTransport(httpInternal.TransportOptions{
Dsn: client.options.Dsn,
HTTPClient: client.options.HTTPClient,
HTTPTransport: client.options.HTTPTransport,
HTTPProxy: client.options.HTTPProxy,
HTTPSProxy: client.options.HTTPSProxy,
CaCerts: client.options.CaCerts,
})
client.Transport = &internalAsyncTransportAdapter{transport: transport}

storage := map[ratelimit.Category]telemetry.Storage[protocol.EnvelopeItemConvertible]{
ratelimit.CategoryError: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryError, 100, telemetry.OverflowPolicyDropOldest, 1, 0),
ratelimit.CategoryTransaction: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryTransaction, 1000, telemetry.OverflowPolicyDropOldest, 1, 0),
ratelimit.CategoryLog: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryLog, 100, telemetry.OverflowPolicyDropOldest, 100, 5*time.Second),
ratelimit.CategoryMonitor: telemetry.NewRingBuffer[protocol.EnvelopeItemConvertible](ratelimit.CategoryMonitor, 100, telemetry.OverflowPolicyDropOldest, 1, 0),
}

sdkInfo := &protocol.SdkInfo{
Name: client.sdkIdentifier,
Version: client.sdkVersion,
}

client.telemetryBuffer = telemetry.NewBuffer(storage, transport, &client.dsn.Dsn, sdkInfo)
}

func (client *Client) setupIntegrations() {
integrations := []Integration{
new(contextifyFramesIntegration),
Expand Down Expand Up @@ -531,7 +576,7 @@ func (client *Client) RecoverWithContext(
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (client *Client) Flush(timeout time.Duration) bool {
if client.batchLogger != nil {
if client.batchLogger != nil || client.telemetryBuffer != nil {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return client.FlushWithContext(ctx)
Expand All @@ -555,6 +600,9 @@ func (client *Client) FlushWithContext(ctx context.Context) bool {
if client.batchLogger != nil {
client.batchLogger.Flush(ctx.Done())
}
if client.telemetryBuffer != nil {
return client.telemetryBuffer.FlushWithContext(ctx)
}
return client.Transport.FlushWithContext(ctx)
}

Expand All @@ -563,6 +611,9 @@ func (client *Client) FlushWithContext(ctx context.Context) bool {
// Close should be called after Flush and before terminating the program
// otherwise some events may be lost.
func (client *Client) Close() {
if client.telemetryBuffer != nil {
client.telemetryBuffer.Close(5 * time.Second)
}
client.Transport.Close()
}

Expand Down Expand Up @@ -683,7 +734,13 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
}
}

client.Transport.SendEvent(event)
if client.telemetryBuffer != nil {
if !client.telemetryBuffer.Add(event) {
debuglog.Println("Event dropped: telemetry buffer full or unavailable")
}
} else {
client.Transport.SendEvent(event)
}

return &event.EventID
}
Expand Down
7 changes: 4 additions & 3 deletions hub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/getsentry/sentry-go/internal/protocol"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
Expand Down Expand Up @@ -177,9 +178,9 @@ func TestConfigureScope(t *testing.T) {
}

func TestLastEventID(t *testing.T) {
uuid := EventID(uuid())
hub := &Hub{lastEventID: uuid}
assertEqual(t, uuid, hub.LastEventID())
eventID := EventID(protocol.GenerateEventID())
hub := &Hub{lastEventID: eventID}
assertEqual(t, eventID, hub.LastEventID())
}

func TestLastEventIDUpdatesAfterCaptures(t *testing.T) {
Expand Down
127 changes: 127 additions & 0 deletions interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,70 @@ func (e *Event) ToEnvelopeWithTime(dsn *protocol.Dsn, sentAt time.Time) (*protoc
return envelope, nil
}

// ToEnvelopeItem converts the Event to a Sentry envelope item.
func (e *Event) ToEnvelopeItem() (*protocol.EnvelopeItem, error) {
eventBody, err := json.Marshal(e)
if err != nil {
// Try fallback: remove problematic fields and retry
e.Breadcrumbs = nil
e.Contexts = nil
e.Extra = map[string]interface{}{
"info": fmt.Sprintf("Could not encode original event as JSON. "+
"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
"Please verify the data you attach to the scope. "+
"Error: %s", err),
}

eventBody, err = json.Marshal(e)
if err != nil {
return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err)
}

DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields")
}
Copy link

Choose a reason for hiding this comment

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

Bug: Event Mutation During JSON Fallback

The ToEnvelopeItem method mutates the original Event object during its JSON marshaling fallback. This modification, affecting Breadcrumbs, Contexts, and Extra, can cause data loss or unexpected behavior if the Event is reused.

Fix in Cursor Fix in Web


// TODO: all event types should be abstracted to implement EnvelopeItemConvertible and convert themselves.
var item *protocol.EnvelopeItem
switch e.Type {
case transactionType:
item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody)
case checkInType:
item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody)
case logEvent.Type:
item = protocol.NewLogItem(len(e.Logs), eventBody)
default:
item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody)
}

return item, nil
}

// GetCategory returns the rate limit category for this event.
func (e *Event) GetCategory() ratelimit.Category {
return e.toCategory()
}

// GetEventID returns the event ID.
func (e *Event) GetEventID() string {
return string(e.EventID)
}

// GetSdkInfo returns SDK information for the envelope header.
func (e *Event) GetSdkInfo() *protocol.SdkInfo {
return &e.Sdk
}

// GetDynamicSamplingContext returns trace context for the envelope header.
func (e *Event) GetDynamicSamplingContext() map[string]string {
trace := make(map[string]string)
if dsc := e.sdkMetaData.dsc; dsc.HasEntries() {
for k, v := range dsc.Entries {
trace[k] = v
}
}
return trace
}

// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
// to prevent accidentally storing T when we mean *T.
// For example, the TraceContext must be stored as *TraceContext to pick up the
Expand Down Expand Up @@ -667,6 +731,69 @@ type Log struct {
Attributes map[string]Attribute `json:"attributes,omitempty"`
}

// ToEnvelopeItem converts the Log to a Sentry envelope item for batching.
func (l *Log) ToEnvelopeItem() (*protocol.EnvelopeItem, error) {
type logJSON struct {
Timestamp *float64 `json:"timestamp,omitempty"`
TraceID string `json:"trace_id,omitempty"`
Level string `json:"level"`
Severity int `json:"severity_number,omitempty"`
Body string `json:"body,omitempty"`
Attributes map[string]protocol.LogAttribute `json:"attributes,omitempty"`
}
Comment on lines +736 to +743
Copy link
Member

Choose a reason for hiding this comment

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

Hmm I see the reason why we need this type now, e.g. Timestamp is a time.Time but you want to serialize it as float64, etc.
In Rust (serde) you could do this using serialize_with: https://serde.rs/field-attrs.html#serialize_with 😎


// Convert time.Time to seconds float if set
var ts *float64
if !l.Timestamp.IsZero() {
sec := float64(l.Timestamp.UnixNano()) / 1e9
ts = &sec
}

attrs := make(map[string]protocol.LogAttribute, len(l.Attributes))
for k, v := range l.Attributes {
attrs[k] = protocol.LogAttribute{Value: v.Value, Type: string(v.Type)}
}

logData, err := json.Marshal(logJSON{
Timestamp: ts,
TraceID: l.TraceID.String(),
Level: string(l.Level),
Severity: l.Severity,
Body: l.Body,
Attributes: attrs,
})
if err != nil {
return nil, err
}

return &protocol.EnvelopeItem{
Header: &protocol.EnvelopeItemHeader{
Type: protocol.EnvelopeItemTypeLog,
},
Payload: logData,
}, nil
}

// GetCategory returns the rate limit category for logs.
func (l *Log) GetCategory() ratelimit.Category {
return ratelimit.CategoryLog
}

// GetEventID returns empty string (event ID set when batching).
func (l *Log) GetEventID() string {
return ""
}

// GetSdkInfo returns nil (SDK info set when batching).
func (l *Log) GetSdkInfo() *protocol.SdkInfo {
return nil
}

// GetDynamicSamplingContext returns nil (trace context set when batching).
func (l *Log) GetDynamicSamplingContext() map[string]string {
return nil
}

type AttrType string

const (
Expand Down
Loading
Loading