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
+642
View File
@@ -0,0 +1,642 @@
package http
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net/http"
"net/url"
"sync"
"time"
"github.com/getsentry/sentry-go/internal/debuglog"
"github.com/getsentry/sentry-go/internal/protocol"
"github.com/getsentry/sentry-go/internal/ratelimit"
"github.com/getsentry/sentry-go/internal/util"
"github.com/getsentry/sentry-go/report"
)
const (
apiVersion = 7
defaultTimeout = time.Second * 30
defaultQueueSize = 1000
defaultClientReportsTick = time.Second * 30
)
var (
ErrTransportQueueFull = errors.New("transport queue full")
ErrTransportClosed = errors.New("transport is closed")
ErrEmptyEnvelope = errors.New("empty envelope provided")
)
type TransportOptions struct {
Dsn string
HTTPClient *http.Client
HTTPTransport http.RoundTripper
HTTPProxy string
HTTPSProxy string
CaCerts *x509.CertPool
Recorder report.ClientReportRecorder
Provider report.ClientReportProvider
SdkInfo func() *protocol.SdkInfo
}
func getProxyConfig(options TransportOptions) func(*http.Request) (*url.URL, error) {
if len(options.HTTPSProxy) > 0 {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPSProxy)
}
}
if len(options.HTTPProxy) > 0 {
return func(*http.Request) (*url.URL, error) {
return url.Parse(options.HTTPProxy)
}
}
return http.ProxyFromEnvironment
}
func getTLSConfig(options TransportOptions) *tls.Config {
if options.CaCerts != nil {
return &tls.Config{
RootCAs: options.CaCerts,
MinVersion: tls.VersionTLS12,
}
}
return nil
}
func getSentryRequestFromEnvelope(ctx context.Context, dsn *protocol.Dsn, envelope *protocol.Envelope) (r *http.Request, err error) {
defer func() {
if r != nil {
var sdkName, sdkVersion string
if envelope.Header.Sdk != nil {
sdkVersion = envelope.Header.Sdk.Version
sdkName = envelope.Header.Sdk.Name
}
r.Header.Set("User-Agent", fmt.Sprintf("%s/%s", sdkName, sdkVersion))
r.Header.Set("Content-Type", "application/x-sentry-envelope")
auth := fmt.Sprintf("Sentry sentry_version=%d, "+
"sentry_client=%s/%s, sentry_key=%s", apiVersion, sdkName, sdkVersion, dsn.GetPublicKey())
if dsn.GetSecretKey() != "" {
auth = fmt.Sprintf("%s, sentry_secret=%s", auth, dsn.GetSecretKey())
}
r.Header.Set("X-Sentry-Auth", auth)
}
}()
var buf bytes.Buffer
_, err = envelope.WriteTo(&buf)
if err != nil {
return nil, err
}
return http.NewRequestWithContext(
ctx,
http.MethodPost,
dsn.GetAPIURL().String(),
&buf,
)
}
func categoryFromEnvelope(envelope *protocol.Envelope) ratelimit.Category {
if envelope == nil || len(envelope.Items) == 0 {
return ratelimit.CategoryAll
}
for _, item := range envelope.Items {
if item == nil || item.Header == nil {
continue
}
switch item.Header.Type {
case protocol.EnvelopeItemTypeEvent:
return ratelimit.CategoryError
case protocol.EnvelopeItemTypeTransaction:
return ratelimit.CategoryTransaction
case protocol.EnvelopeItemTypeCheckIn:
return ratelimit.CategoryMonitor
case protocol.EnvelopeItemTypeLog:
return ratelimit.CategoryLog
case protocol.EnvelopeItemTypeAttachment:
continue
default:
return ratelimit.CategoryAll
}
}
return ratelimit.CategoryAll
}
// SyncTransport is a blocking implementation of Transport.
//
// Clients using this transport will send requests to Sentry sequentially and
// block until a response is returned.
//
// The blocking behavior is useful in a limited set of use cases. For example,
// use it when deploying code to a Function as a Service ("Serverless")
// platform, where any work happening in a background goroutine is not
// guaranteed to execute.
//
// For most cases, prefer AsyncTransport.
type SyncTransport struct {
dsn *protocol.Dsn
client *http.Client
transport http.RoundTripper
recorder report.ClientReportRecorder
provider report.ClientReportProvider
sdkInfo func() *protocol.SdkInfo
mu sync.Mutex
limits ratelimit.Map
Timeout time.Duration
}
func NewSyncTransport(options TransportOptions) protocol.TelemetryTransport {
dsn, err := protocol.NewDsn(options.Dsn)
if err != nil || dsn == nil {
debuglog.Printf("Transport is disabled: invalid dsn: %v\n", err)
return NewNoopTransport()
}
recorder := options.Recorder
if recorder == nil {
recorder = report.NoopRecorder()
}
provider := options.Provider
if provider == nil {
provider = report.NoopProvider()
}
transport := &SyncTransport{
Timeout: defaultTimeout,
limits: make(ratelimit.Map),
dsn: dsn,
recorder: recorder,
provider: provider,
sdkInfo: options.SdkInfo,
}
if options.HTTPTransport != nil {
transport.transport = options.HTTPTransport
} else {
transport.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
transport.client = options.HTTPClient
} else {
transport.client = &http.Client{
Transport: transport.transport,
Timeout: transport.Timeout,
}
}
return transport
}
func (t *SyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
return t.SendEnvelopeWithContext(context.Background(), envelope)
}
func (t *SyncTransport) Close() {}
func (t *SyncTransport) IsRateLimited(category ratelimit.Category) bool {
return t.disabled(category)
}
func (t *SyncTransport) HasCapacity() bool { return true }
func (t *SyncTransport) SendEnvelopeWithContext(ctx context.Context, envelope *protocol.Envelope) error {
if envelope == nil || len(envelope.Items) == 0 {
return ErrEmptyEnvelope
}
category := categoryFromEnvelope(envelope)
if t.disabled(category) {
t.recorder.RecordForEnvelope(report.ReasonRateLimitBackoff, envelope)
return nil
}
// the sync transport needs to attach client reports when available
t.provider.AttachToEnvelope(envelope)
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
if err != nil {
debuglog.Printf("There was an issue creating the request: %v", err)
t.recorder.RecordForEnvelope(report.ReasonInternalError, envelope)
return err
}
identifier := util.EnvelopeIdentifier(envelope)
debuglog.Printf(
"Sending %s to %s project: %s",
identifier,
t.dsn.GetHost(),
t.dsn.GetProjectID(),
)
result, err := util.DoSendRequest(t.client, request, identifier)
if err != nil {
debuglog.Printf("There was an issue with sending an event: %v", err)
t.recorder.RecordForEnvelope(report.ReasonNetworkError, envelope)
return err
}
if result.IsSendError() {
t.recorder.RecordForEnvelope(report.ReasonSendError, envelope)
}
t.mu.Lock()
t.limits.Merge(result.Limits)
t.mu.Unlock()
return nil
}
func (t *SyncTransport) Flush(_ time.Duration) bool {
return true
}
func (t *SyncTransport) FlushWithContext(_ context.Context) bool {
return true
}
func (t *SyncTransport) disabled(c ratelimit.Category) bool {
t.mu.Lock()
defer t.mu.Unlock()
disabled := t.limits.IsRateLimited(c)
if disabled {
debuglog.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
}
return disabled
}
// AsyncTransport is the default, non-blocking, implementation of Transport.
//
// Clients using this transport will enqueue requests in a queue and return to
// the caller before any network communication has happened. Requests are sent
// to Sentry sequentially from a background goroutine.
type AsyncTransport struct {
dsn *protocol.Dsn
client *http.Client
transport http.RoundTripper
recorder report.ClientReportRecorder
provider report.ClientReportProvider
sdkInfo func() *protocol.SdkInfo
queue chan *protocol.Envelope
mu sync.RWMutex
limits ratelimit.Map
done chan struct{}
wg sync.WaitGroup
flushRequest chan chan struct{}
closeMu sync.RWMutex
QueueSize int
Timeout time.Duration
startOnce sync.Once
closeOnce sync.Once
}
func NewAsyncTransport(options TransportOptions) protocol.TelemetryTransport {
dsn, err := protocol.NewDsn(options.Dsn)
if err != nil || dsn == nil {
debuglog.Printf("Transport is disabled: invalid dsn: %v", err)
return NewNoopTransport()
}
recorder := options.Recorder
if recorder == nil {
recorder = report.NoopRecorder()
}
provider := options.Provider
if provider == nil {
provider = report.NoopProvider()
}
transport := &AsyncTransport{
QueueSize: defaultQueueSize,
Timeout: defaultTimeout,
done: make(chan struct{}),
limits: make(ratelimit.Map),
dsn: dsn,
recorder: recorder,
provider: provider,
sdkInfo: options.SdkInfo,
}
transport.queue = make(chan *protocol.Envelope, transport.QueueSize)
transport.flushRequest = make(chan chan struct{})
if options.HTTPTransport != nil {
transport.transport = options.HTTPTransport
} else {
transport.transport = &http.Transport{
Proxy: getProxyConfig(options),
TLSClientConfig: getTLSConfig(options),
}
}
if options.HTTPClient != nil {
transport.client = options.HTTPClient
} else {
transport.client = &http.Client{
Transport: transport.transport,
Timeout: transport.Timeout,
}
}
transport.start()
return transport
}
func (t *AsyncTransport) start() {
t.startOnce.Do(func() {
if t.recorder == nil {
t.recorder = report.NoopRecorder()
}
if t.provider == nil {
t.provider = report.NoopProvider()
}
t.wg.Add(1)
go t.worker()
})
}
// HasCapacity reports whether the async transport queue appears to have space
// for at least one more envelope. This is a best-effort, non-blocking check.
func (t *AsyncTransport) HasCapacity() bool {
t.mu.RLock()
defer t.mu.RUnlock()
select {
case <-t.done:
return false
default:
}
return len(t.queue) < cap(t.queue)
}
func (t *AsyncTransport) SendEnvelope(envelope *protocol.Envelope) error {
t.closeMu.RLock()
defer t.closeMu.RUnlock()
select {
case <-t.done:
return ErrTransportClosed
default:
}
if envelope == nil || len(envelope.Items) == 0 {
return ErrEmptyEnvelope
}
category := categoryFromEnvelope(envelope)
if t.isRateLimited(category) {
t.recorder.RecordForEnvelope(report.ReasonRateLimitBackoff, envelope)
return nil
}
identifier := util.EnvelopeIdentifier(envelope)
select {
case <-t.done:
return ErrTransportClosed
case t.queue <- envelope:
debuglog.Printf(
"Sending %s to %s project: %s",
identifier,
t.dsn.GetHost(),
t.dsn.GetProjectID(),
)
return nil
default:
t.recorder.RecordForEnvelope(report.ReasonQueueOverflow, envelope)
return ErrTransportQueueFull
}
}
func (t *AsyncTransport) Flush(timeout time.Duration) bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return t.FlushWithContext(ctx)
}
func (t *AsyncTransport) FlushWithContext(ctx context.Context) bool {
t.closeMu.RLock()
defer t.closeMu.RUnlock()
flushResponse := make(chan struct{})
select {
case <-t.done:
debuglog.Println("Failed to flush, transport is closed.")
return false
case t.flushRequest <- flushResponse:
select {
case <-flushResponse:
debuglog.Println("Buffer flushed successfully.")
return true
case <-ctx.Done():
debuglog.Println("Failed to flush, buffer timed out.")
return false
}
case <-ctx.Done():
debuglog.Println("Failed to flush, buffer timed out.")
return false
}
}
func (t *AsyncTransport) Close() {
t.closeOnce.Do(func() {
t.closeMu.Lock()
defer t.closeMu.Unlock()
close(t.done)
t.wg.Wait()
})
}
func (t *AsyncTransport) IsRateLimited(category ratelimit.Category) bool {
return t.isRateLimited(category)
}
func (t *AsyncTransport) resolveSdkInfo() *protocol.SdkInfo {
if t.sdkInfo == nil {
return &protocol.SdkInfo{}
}
return t.sdkInfo()
}
func (t *AsyncTransport) worker() {
defer t.wg.Done()
crTicker := time.NewTicker(defaultClientReportsTick)
defer crTicker.Stop()
for {
select {
case <-t.done:
return
case <-crTicker.C:
t.sendClientReport()
case envelope, open := <-t.queue:
if !open {
return
}
t.sendEnvelopeHTTP(envelope)
case flushResponse, open := <-t.flushRequest:
if !open {
return
}
t.drainQueue()
close(flushResponse)
}
}
}
// sendClientReport sends a standalone envelope containing only a client report.
func (t *AsyncTransport) sendClientReport() {
r := t.provider.TakeReport()
if r == nil {
return
}
item, err := r.ToEnvelopeItem()
if err != nil {
debuglog.Printf("Failed to serialize client report: %v", err)
return
}
header := &protocol.EnvelopeHeader{
SentAt: time.Now(),
Dsn: t.dsn,
Sdk: t.resolveSdkInfo(),
}
envelope := protocol.NewEnvelope(header)
envelope.AddItem(item)
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
if err != nil {
debuglog.Printf("Failed to create client report request: %v", err)
return
}
result, err := util.DoSendRequest(t.client, request, "client report")
if err != nil {
debuglog.Printf("Failed to send client report: %v", err)
return
}
t.mu.Lock()
t.limits.Merge(result.Limits)
t.mu.Unlock()
}
func (t *AsyncTransport) drainQueue() {
for {
select {
case envelope, open := <-t.queue:
if !open {
return
}
t.sendEnvelopeHTTP(envelope)
default:
return
}
}
}
func (t *AsyncTransport) sendEnvelopeHTTP(envelope *protocol.Envelope) bool { //nolint: unparam
category := categoryFromEnvelope(envelope)
if t.isRateLimited(category) {
t.recorder.RecordForEnvelope(report.ReasonRateLimitBackoff, envelope)
return false
}
// attach to envelope after rate-limit check
t.provider.AttachToEnvelope(envelope)
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
request, err := getSentryRequestFromEnvelope(ctx, t.dsn, envelope)
if err != nil {
debuglog.Printf("Failed to create request from envelope: %v", err)
t.recorder.RecordForEnvelope(report.ReasonInternalError, envelope)
return false
}
identifier := util.EnvelopeIdentifier(envelope)
result, err := util.DoSendRequest(t.client, request, identifier)
if err != nil {
debuglog.Printf("HTTP request failed: %v", err)
t.recorder.RecordForEnvelope(report.ReasonNetworkError, envelope)
return false
}
if result.IsSendError() {
t.recorder.RecordForEnvelope(report.ReasonSendError, envelope)
}
t.mu.Lock()
t.limits.Merge(result.Limits)
t.mu.Unlock()
return result.Success
}
func (t *AsyncTransport) isRateLimited(category ratelimit.Category) bool {
t.mu.RLock()
defer t.mu.RUnlock()
limited := t.limits.IsRateLimited(category)
if limited {
debuglog.Printf("Rate limited for category %q until %v", category, t.limits.Deadline(category))
}
return limited
}
// NoopTransport is a transport implementation that drops all events.
// Used internally when an empty or invalid DSN is provided.
type NoopTransport struct{}
func NewNoopTransport() *NoopTransport {
debuglog.Println("Transport initialized with invalid DSN. Using NoopTransport. No events will be delivered.")
return &NoopTransport{}
}
func (t *NoopTransport) SendEnvelope(_ *protocol.Envelope) error {
debuglog.Println("Envelope dropped due to NoopTransport usage.")
return nil
}
func (t *NoopTransport) IsRateLimited(_ ratelimit.Category) bool {
return false
}
func (t *NoopTransport) Flush(_ time.Duration) bool {
return true
}
func (t *NoopTransport) FlushWithContext(_ context.Context) bool {
return true
}
func (t *NoopTransport) Close() {
// Nothing to close
}
func (t *NoopTransport) HasCapacity() bool { return true }