Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ The SDK supports reporting errors and tracking application performance.
To get started, have a look at one of our [examples](_examples/):
- [Basic error instrumentation](_examples/basic/main.go)
- [Error and tracing for HTTP servers](_examples/http/main.go)
- [Local development debugging with Spotlight](_examples/spotlight/main.go)

We also provide a [complete API reference](https://pkg.go.dev/github.com/getsentry/sentry-go).

Expand Down
86 changes: 86 additions & 0 deletions _examples/spotlight/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// This is an example program that demonstrates Sentry Go SDK integration
// with Spotlight for local development debugging.
//
// Spotlight allows you to see all events captured by your application in a
// local development web UI, without sending them to a Sentry server. This is
// useful for debugging during development.
//
// Try it by running:
//
// go run main.go
//
// Configuration:
// - Spotlight is enabled by default in this example (Spotlight: true)
// - Events are NOT sent to Sentry (DSN is empty)
// - To also send events to Sentry, set DSN via environment variable:
// SENTRY_DSN=https://key@sentry.io/project go run main.go
// - Or edit the DSN field below
//
// Before running this example, make sure Spotlight is running:
//
// npm install -g @spotlightjs/spotlight
// spotlight
//
// Then open http://localhost:8969 in your browser to see the Spotlight UI.
package main

import (
"context"
"errors"
"log"
"time"

"github.com/getsentry/sentry-go"
)

func main() {
err := sentry.Init(sentry.ClientOptions{
// Either set your DSN here or set the SENTRY_DSN environment variable.
Dsn: "",
// Enable printing of SDK debug messages.
// Useful when getting started or trying to figure something out.
Debug: true,
// Enable Spotlight for local debugging.
Spotlight: true,
// Enable tracing to see performance data in Spotlight.
EnableTracing: true,
TracesSampleRate: 1.0,
})
if err != nil {
log.Fatalf("sentry.Init: %s", err)
}
// Flush buffered events before the program terminates.
// Set the timeout to the maximum duration the program can afford to wait.
defer sentry.Flush(2 * time.Second)

log.Println("Sending sample events to Spotlight...")

// Capture a simple message
sentry.CaptureMessage("Hello from Spotlight!")

// Capture an exception
sentry.CaptureException(errors.New("example error for Spotlight debugging"))

// Capture an event with additional context
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetTag("environment", "development")
scope.SetLevel(sentry.LevelWarning)
scope.SetContext("example", map[string]interface{}{
"feature": "spotlight_integration",
"version": "1.0.0",
})
sentry.CaptureMessage("Event with additional context")
})

// Performance monitoring example
span := sentry.StartSpan(context.Background(), "example.operation")
defer span.Finish()

span.SetData("example", "data")
childSpan := span.StartChild("child.operation")
// Simulate some work
time.Sleep(100 * time.Millisecond)
childSpan.Finish()

log.Println("Events sent! Check your Spotlight UI at http://localhost:8969")
}
124 changes: 124 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,10 +279,43 @@ type ClientOptions struct {
// IMPORTANT: to not ignore any status codes, the option should be an empty slice and not nil. The nil option is
// used for defaulting to 404 ignores.
TraceIgnoreStatusCodes [][]int
// Enable Spotlight for local development debugging.
// When enabled, events are sent to the local Spotlight sidecar.
// Default Spotlight URL is http://localhost:8969/stream
Spotlight bool
// SpotlightURL is the URL to send events to when Spotlight is enabled.
// Defaults to http://localhost:8969/stream
SpotlightURL string
// DisableTelemetryBuffer disables the telemetry buffer layer for prioritizing events and uses the old transport layer.
DisableTelemetryBuffer bool
}

// spotlightConfigValue represents the parsed result of SENTRY_SPOTLIGHT env var or config.
type spotlightConfigValue struct {
enabled bool
url string
}

// parseSpotlightEnvVar parses the SENTRY_SPOTLIGHT environment variable.
// Truthy values ("true", "t", "y", "yes", "on", "1") enable Spotlight with the default URL.
// Falsy values ("false", "f", "n", "no", "off", "0") disable it.
// Any other non-empty string is treated as a custom Spotlight URL.
func parseSpotlightEnvVar(value string) spotlightConfigValue {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return spotlightConfigValue{enabled: false}
}

switch strings.ToLower(trimmed) {
case "true", "t", "y", "yes", "on", "1":
return spotlightConfigValue{enabled: true}
case "false", "f", "n", "no", "off", "0":
return spotlightConfigValue{enabled: false}
}

return spotlightConfigValue{enabled: true, url: trimmed}
}

// Client is the underlying processor that is used by the main API and Hub
// instances. It must be created with NewClient.
type Client struct {
Expand Down Expand Up @@ -365,6 +398,47 @@ func NewClient(options ClientOptions) (*Client, error) {
options.TraceIgnoreStatusCodes = [][]int{{404}}
}

// Handle Spotlight configuration with environment variable precedence
spotlightEnvVar := os.Getenv("SENTRY_SPOTLIGHT")
if spotlightEnvVar != "" {
envConfig := parseSpotlightEnvVar(spotlightEnvVar)

switch {
case options.SpotlightURL != "":
// Config URL explicitly set: use it and warn
debuglog.Printf("Both SpotlightURL config and SENTRY_SPOTLIGHT env var are set. Using config URL: %s", options.SpotlightURL)
options.Spotlight = true
case options.Spotlight && envConfig.url != "":
// Config enables Spotlight but no URL, env var has URL: use env var URL
options.SpotlightURL = envConfig.url
debuglog.Printf("Spotlight enabled via config but using URL from SENTRY_SPOTLIGHT: %s", envConfig.url)
case !options.Spotlight:
// Config doesn't set Spotlight: use env var setting
options.Spotlight = envConfig.enabled
if envConfig.url != "" {
options.SpotlightURL = envConfig.url
}
if envConfig.enabled {
debuglog.Println("Spotlight enabled via SENTRY_SPOTLIGHT env var")
} else {
debuglog.Println("Spotlight disabled via SENTRY_SPOTLIGHT env var")
}
}
}

// Spotlight is a local development tool: always deliver 100% of events
// with full PII so the developer sees everything their app is generating.
if options.Spotlight {
if options.SampleRate != 1.0 {
debuglog.Printf("Overriding SampleRate from %.2f to 1.0 for Spotlight", options.SampleRate)
options.SampleRate = 1.0
}
if !options.SendDefaultPII {
debuglog.Println("Enabling SendDefaultPII for Spotlight")
options.SendDefaultPII = true
}
}

// SENTRYGODEBUG is a comma-separated list of key=value pairs (similar
// to GODEBUG). It is not a supported feature: recognized debug options
// may change any time.
Expand Down Expand Up @@ -463,6 +537,10 @@ func (client *Client) setupTransport() {
}
}

if opts.Spotlight {
transport = NewSpotlightTransport(transport)
}
Comment on lines +540 to +542
Copy link
Contributor

Choose a reason for hiding this comment

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

we should also override the sample rate to be 1.0 and PII to true when spotlight is enabled


transport.Configure(opts)
client.Transport = transport
}
Expand Down Expand Up @@ -528,6 +606,7 @@ func (client *Client) setupIntegrations() {
new(ignoreErrorsIntegration),
new(ignoreTransactionsIntegration),
new(globalTagsIntegration),
new(spotlightIntegration),
}

if client.options.Integrations != nil {
Expand Down Expand Up @@ -907,6 +986,12 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
debuglog.Println("Event dropped: telemetry buffer full or unavailable")
}
} else {
// For Spotlight: build and send envelope for enhanced data collection
if client.options.Spotlight {
envelope := client.buildEnvelopeFromEvent(event)
client.Transport.SendEnvelope(envelope)
}
// Default path: send event directly for backwards compatibility
Copy link

Choose a reason for hiding this comment

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

Events sent twice to both Sentry and Spotlight

High Severity

When Spotlight is enabled, processEvent calls both SendEnvelope and SendEvent on client.Transport, which is a SpotlightTransport. Each of those methods forwards to the underlying transport AND sends to Spotlight. Since HTTPTransport.SendEnvelope internally deserializes and calls SendEvent, the event is delivered to Sentry twice and to Spotlight twice. The SendEnvelope call at line 992 and the SendEvent call at line 995 are redundant — only one path is needed.

Additional Locations (1)
Fix in Cursor Fix in Web

client.Transport.SendEvent(event)
Comment on lines +989 to 995
Copy link

Choose a reason for hiding this comment

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

Bug: When Spotlight is enabled, processEvent calls both SendEnvelope and SendEvent on the transport, causing each event to be sent to Sentry twice.
Severity: HIGH

Suggested Fix

In processEvent, modify the logic to only call client.Transport.SendEnvelope(envelope) when client.options.Spotlight is true. The SendEnvelope method on the transport should be solely responsible for forwarding to both Spotlight and Sentry. The redundant call to client.Transport.SendEvent(event) should be removed.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: client.go#L989-L995

Potential issue: When Spotlight is enabled and `telemetryProcessor` is `nil`, the
`processEvent` function calls both `client.Transport.SendEnvelope()` and
`client.Transport.SendEvent()`. The `client.Transport` is a `SpotlightTransport` which
wraps an underlying transport like `HTTPTransport`. The
`SpotlightTransport.SendEnvelope` call results in the underlying transport sending the
event to Sentry. The subsequent `SpotlightTransport.SendEvent` call sends the same event
to Sentry again. This results in every captured event being sent to Sentry twice.

Did we get this right? 👍 / 👎 to inform future reviews.

}

Expand Down Expand Up @@ -1027,6 +1112,45 @@ func (client *Client) integrationAlreadyInstalled(name string) bool {
return false
}

// buildEnvelopeFromEvent builds an envelope from an event.
// This is used to send events via the envelope-based transport API.
func (client *Client) buildEnvelopeFromEvent(event *Event) *protocol.Envelope {
header := &protocol.EnvelopeHeader{
EventID: string(event.EventID),
SentAt: time.Now(),
Sdk: &protocol.SdkInfo{
Name: event.Sdk.Name,
Version: event.Sdk.Version,
},
}

if client.dsn != nil {
header.Dsn = client.dsn.String()
}

if header.EventID == "" {
header.EventID = protocol.GenerateEventID()
}

envelope := protocol.NewEnvelope(header)

// Convert event to envelope item
item, err := event.ToEnvelopeItem()
if err != nil {
debuglog.Printf("Failed to convert event to envelope item: %v", err)
return envelope
}
envelope.AddItem(item)

// Add attachments
for _, attachment := range event.Attachments {
attachmentItem := protocol.NewAttachmentItem(attachment.Filename, attachment.ContentType, attachment.Payload)
envelope.AddItem(attachmentItem)
}

return envelope
}

// sample returns true with the given probability, which must be in the range
// [0.0, 1.0].
func sample(probability float64) bool {
Expand Down
8 changes: 8 additions & 0 deletions hub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,10 @@ func TestHub_Flush(t *testing.T) {
if gotEvents[0].Message != wantEvent.Message {
t.Fatalf("expected message to be %v, got %v", wantEvent.Message, gotEvents[0].Message)
}

if transport.FlushCount() != 1 {
t.Fatalf("expected transport.Flush called 1 time, got %d", transport.FlushCount())
}
}

func TestHub_Flush_NoClient(t *testing.T) {
Expand Down Expand Up @@ -540,4 +544,8 @@ func TestHub_FlushWithContext(t *testing.T) {
if gotEvents[0].Message != wantEvent.Message {
t.Fatalf("expected message to be %v, got %v", wantEvent.Message, gotEvents[0].Message)
}

if transport.FlushCount() != 1 {
t.Fatalf("expected transport.FlushWithContext called 1 time, got %d", transport.FlushCount())
}
}
27 changes: 27 additions & 0 deletions integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,30 @@ func loadEnvTags() map[string]string {
}
return tags
}

// ================================
// Spotlight Integration
// ================================

type spotlightIntegration struct{}

func (si *spotlightIntegration) Name() string {
return "Spotlight"
}

func (si *spotlightIntegration) SetupOnce(client *Client) {
// The spotlight integration doesn't add event processors.
// It works by wrapping the transport in setupTransport().
// This integration is mainly for completeness and debugging visibility.
if client.options.Spotlight {
DebugLogger.Printf("Spotlight integration enabled. Events will be sent to %s",
client.getSpotlightURL())
}
}

func (client *Client) getSpotlightURL() string {
if client.options.SpotlightURL != "" {
return client.options.SpotlightURL
}
return "http://localhost:8969/stream"
}
Loading
Loading