fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
+169
View File
@@ -0,0 +1,169 @@
package report
import (
"sync"
"sync/atomic"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
// Aggregator collects discarded event outcomes for client reports.
// Uses atomic operations to be safe for concurrent use.
type Aggregator struct {
mu sync.Mutex
outcomes map[OutcomeKey]*atomic.Int64
}
// NewAggregator creates a new client report Aggregator.
func NewAggregator() *Aggregator {
a := &Aggregator{
outcomes: make(map[OutcomeKey]*atomic.Int64),
}
return a
}
// Record records a discarded event outcome.
func (a *Aggregator) Record(reason DiscardReason, category ratelimit.Category, quantity int64) {
if a == nil || quantity <= 0 {
return
}
key := OutcomeKey{Reason: reason, Category: category}
a.mu.Lock()
defer a.mu.Unlock()
counter, exists := a.outcomes[key]
if !exists {
counter = &atomic.Int64{}
a.outcomes[key] = counter
}
counter.Add(quantity)
}
// RecordOne is a helper method to record one discarded event outcome.
func (a *Aggregator) RecordOne(reason DiscardReason, category ratelimit.Category) {
a.Record(reason, category, 1)
}
// TakeReport atomically takes all accumulated outcomes and returns a ClientReport.
func (a *Aggregator) TakeReport() *ClientReport {
if a == nil {
return nil
}
a.mu.Lock()
defer a.mu.Unlock()
if len(a.outcomes) == 0 {
return nil
}
var events []DiscardedEvent
for key, counter := range a.outcomes {
quantity := counter.Swap(0)
if quantity > 0 {
events = append(events, DiscardedEvent{
Reason: key.Reason,
Category: key.Category,
Quantity: quantity,
})
}
}
// Clear empty counters to prevent unbounded growth
for key, counter := range a.outcomes {
if counter.Load() == 0 {
delete(a.outcomes, key)
}
}
if len(events) == 0 {
return nil
}
return &ClientReport{
Timestamp: time.Now(),
DiscardedEvents: events,
}
}
// RecordForEnvelope records client report outcomes for all items in the envelope.
// It inspects envelope item headers to derive categories, span counts, and log byte sizes.
func (a *Aggregator) RecordForEnvelope(reason DiscardReason, envelope *protocol.Envelope) {
if a == nil {
return
}
for _, item := range envelope.Items {
if item == nil || item.Header == nil {
continue
}
switch item.Header.Type {
case protocol.EnvelopeItemTypeEvent:
a.RecordOne(reason, ratelimit.CategoryError)
case protocol.EnvelopeItemTypeTransaction:
a.RecordOne(reason, ratelimit.CategoryTransaction)
spanCount := int64(item.Header.SpanCount)
a.Record(reason, ratelimit.CategorySpan, spanCount)
case protocol.EnvelopeItemTypeLog:
if item.Header.ItemCount != nil {
a.Record(reason, ratelimit.CategoryLog, int64(*item.Header.ItemCount))
}
if item.Header.Length != nil {
a.Record(reason, ratelimit.CategoryLogByte, int64(*item.Header.Length))
}
case protocol.EnvelopeItemTypeTraceMetric:
a.RecordOne(reason, ratelimit.CategoryTraceMetric)
case protocol.EnvelopeItemTypeCheckIn:
a.RecordOne(reason, ratelimit.CategoryMonitor)
case protocol.EnvelopeItemTypeAttachment, protocol.EnvelopeItemTypeClientReport:
// Skip — not reportable categories
}
}
}
// RecordItem records outcomes for a telemetry item, including supplementary
// categories (span outcomes for transactions, byte size for logs).
func (a *Aggregator) RecordItem(reason DiscardReason, item protocol.TelemetryItem) {
category := item.GetCategory()
a.RecordOne(reason, category)
// Span outcomes for transactions
if category == ratelimit.CategoryTransaction {
type spanCounter interface{ GetSpanCount() int }
if sc, ok := item.(spanCounter); ok {
if count := sc.GetSpanCount(); count > 0 {
a.Record(reason, ratelimit.CategorySpan, int64(count))
}
}
}
// Byte size outcomes for logs
if category == ratelimit.CategoryLog {
type sizer interface{ ApproximateSize() int }
if s, ok := item.(sizer); ok {
if size := s.ApproximateSize(); size > 0 {
a.Record(reason, ratelimit.CategoryLogByte, int64(size))
}
}
}
}
// AttachToEnvelope adds a client report to the envelope if the Aggregator has outcomes available.
func (a *Aggregator) AttachToEnvelope(envelope *protocol.Envelope) {
if a == nil {
return
}
r := a.TakeReport()
if r != nil {
rItem, err := r.ToEnvelopeItem()
if err == nil {
envelope.AddItem(rItem)
} else {
debuglog.Printf("failed to serialize client report: %v, with err: %v", r, err)
}
}
}
+40
View File
@@ -0,0 +1,40 @@
package report
import (
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
)
// ClientReportRecorder is used by components that need to record lost/discarded events.
type ClientReportRecorder interface {
Record(reason DiscardReason, category ratelimit.Category, quantity int64)
RecordOne(reason DiscardReason, category ratelimit.Category)
RecordForEnvelope(reason DiscardReason, envelope *protocol.Envelope)
RecordItem(reason DiscardReason, item protocol.TelemetryItem)
}
// ClientReportProvider is used by the single component responsible for sending client reports.
type ClientReportProvider interface {
TakeReport() *ClientReport
AttachToEnvelope(envelope *protocol.Envelope)
}
// noopRecorder is a no-op implementation of ClientReportRecorder.
type noopRecorder struct{}
func (noopRecorder) Record(DiscardReason, ratelimit.Category, int64) {}
func (noopRecorder) RecordOne(DiscardReason, ratelimit.Category) {}
func (noopRecorder) RecordForEnvelope(DiscardReason, *protocol.Envelope) {}
func (noopRecorder) RecordItem(DiscardReason, protocol.TelemetryItem) {}
// noopProvider is a no-op implementation of ClientReportProvider.
type noopProvider struct{}
func (noopProvider) TakeReport() *ClientReport { return nil }
func (noopProvider) AttachToEnvelope(_ *protocol.Envelope) {}
// NoopRecorder returns a no-op ClientReportRecorder that silently discards all records.
func NoopRecorder() ClientReportRecorder { return noopRecorder{} }
// NoopProvider returns a no-op ClientReportProvider that always returns nil reports.
func NoopProvider() ClientReportProvider { return noopProvider{} }
+18
View File
@@ -0,0 +1,18 @@
package report
import (
"github.com/getsentry/sentry-go/internal/ratelimit"
)
// OutcomeKey uniquely identifies an outcome bucket for aggregation.
type OutcomeKey struct {
Reason DiscardReason
Category ratelimit.Category
}
// DiscardedEvent represents a single discard event outcome for the OutcomeKey.
type DiscardedEvent struct {
Reason DiscardReason `json:"reason"`
Category ratelimit.Category `json:"category"`
Quantity int64 `json:"quantity"`
}
+38
View File
@@ -0,0 +1,38 @@
package report
// DiscardReason represents why an item was discarded.
type DiscardReason string
const (
// ReasonQueueOverflow indicates the transport queue was full.
ReasonQueueOverflow DiscardReason = "queue_overflow"
// ReasonBufferOverflow indicates that an internal buffer was full.
ReasonBufferOverflow DiscardReason = "buffer_overflow"
// ReasonRateLimitBackoff indicates the item was dropped due to rate limiting.
ReasonRateLimitBackoff DiscardReason = "ratelimit_backoff"
// ReasonBeforeSend indicates the item was dropped due to a BeforeSend callback.
ReasonBeforeSend DiscardReason = "before_send"
// ReasonEventProcessor indicates the item was dropped due to an event processor callback.
ReasonEventProcessor DiscardReason = "event_processor"
// ReasonSampleRate indicates the item was dropped due to sampling.
ReasonSampleRate DiscardReason = "sample_rate"
// ReasonNetworkError indicates an HTTP request failed (connection error).
ReasonNetworkError DiscardReason = "network_error"
// ReasonSendError indicates HTTP returned an error status (4xx, 5xx).
//
// The party that drops an envelope is responsible for counting the event (we skip outcomes that the server records
// `http.StatusTooManyRequests` 429). However, relay does not always record an outcome for oversized envelopes, so
// we accept the trade-off of double counting `http.StatusRequestEntityTooLarge` (413) codes, to record all events.
// For more details https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures
ReasonSendError DiscardReason = "send_error"
// ReasonInternalError indicates an internal SDK error.
ReasonInternalError DiscardReason = "internal_sdk_error"
)
+23
View File
@@ -0,0 +1,23 @@
package report
import (
"encoding/json"
"time"
"github.com/getsentry/sentry-go/internal/protocol"
)
// ClientReport is the payload sent to Sentry for tracking discarded events.
type ClientReport struct {
Timestamp time.Time `json:"timestamp"`
DiscardedEvents []DiscardedEvent `json:"discarded_events"`
}
// ToEnvelopeItem converts the ClientReport to an envelope item.
func (r *ClientReport) ToEnvelopeItem() (*protocol.EnvelopeItem, error) {
payload, err := json.Marshal(r)
if err != nil {
return nil, err
}
return protocol.NewClientReportItem(payload), nil
}