Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,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")
}
6 changes: 3 additions & 3 deletions batch_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ func (p *batchProcessor[T]) Start() {
p.startOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // G118: cancel is stored in p.cancel and called in Shutdown()
p.cancel = cancel
p.wg.Add(1)
go p.run(ctx)
p.wg.Go(func() {
p.run(ctx)
})
})
}

Expand All @@ -78,7 +79,6 @@ func (p *batchProcessor[T]) Shutdown() {
}

func (p *batchProcessor[T]) run(ctx context.Context) {
defer p.wg.Done()
var items []T
timer := time.NewTimer(0)
timer.Stop()
Expand Down
117 changes: 115 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,10 +295,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 @@ -382,6 +415,67 @@ func NewClient(options ClientOptions) (*Client, error) {
options.TraceIgnoreStatusCodes = [][]int{{404}}
}

// A URL always implies Spotlight is enabled, per the Spotlight spec's
// two-attribute configuration approach.
if options.SpotlightURL != "" {
options.Spotlight = true
}

// 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: it already implies Spotlight is
// enabled (above) and takes precedence over the env var.
debuglog.Printf("Both SpotlightURL config and SENTRY_SPOTLIGHT env var are set. Using config URL: %s", options.SpotlightURL)
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-only setups (no DSN) should show everything, so deliver
// 100% of events with full PII. Skip this if a DSN is also set, so
// Spotlight never changes what's sent to real Sentry.
//
// Must run before snapshotDataCollection below, which reads SendDefaultPII.
if options.Spotlight && options.Dsn == "" {
if options.SampleRate != 1.0 {
debuglog.Printf("Overriding SampleRate from %.2f to 1.0 for Spotlight", options.SampleRate)
options.SampleRate = 1.0
}
if options.EnableTracing && options.TracesSampleRate != 1.0 {
debuglog.Printf("Overriding TracesSampleRate from %.2f to 1.0 for Spotlight", options.TracesSampleRate)
options.TracesSampleRate = 1.0
}
Comment thread
cursor[bot] marked this conversation as resolved.
if options.EnableTracing && options.TracesSampler != nil {
// TracesSampler takes precedence over TracesSampleRate, so the
// override above wouldn't help - a custom sampler could still
// drop transactions Spotlight is supposed to see.
debuglog.Println("Disabling TracesSampler for Spotlight so all traces reach it")
options.TracesSampler = nil
}
if !options.SendDefaultPII {
debuglog.Println("Enabling SendDefaultPII for Spotlight")
options.SendDefaultPII = true
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

resolvedDataCollection := snapshotDataCollection(options.DataCollection, options.SendDefaultPII)
options.DataCollection = cloneDataCollection(&resolvedDataCollection)

Expand Down Expand Up @@ -434,10 +528,13 @@ func NewClient(options ClientOptions) (*Client, error) {

// We currently disallow using custom Transport with the new Telemetry Processor, due to the difference in transport signatures.
// The option should be enabled when the new Transport interface signature changes.
// Spotlight uses this same path (its scheduler forwards a copy of every
// envelope to Spotlight too), falling back to the legacy
// SpotlightTransport-wrapped path below only for a custom Transport.
if !options.DisableTelemetryBuffer && client.options.Transport == nil {
client.setupTelemetryProcessor()
} else {
if client.options.Transport != nil {
if client.options.Transport != nil && !options.DisableTelemetryBuffer && !options.Spotlight {
debuglog.Println("Cannot enable Telemetry Processor with custom Transport: fallback to old transport")
}
client.setupTransport()
Expand Down Expand Up @@ -487,6 +584,10 @@ func (client *Client) setupTransport() {
}
}

if opts.Spotlight {
Comment thread
sentry[bot] marked this conversation as resolved.
transport = NewSpotlightTransport(transport)
}
Comment thread
giortzisg marked this conversation as resolved.

transport.Configure(opts)
client.Transport = transport
}
Expand Down Expand Up @@ -514,6 +615,7 @@ func (client *Client) setupTelemetryProcessor() {
Recorder: client.reportRecorder,
Provider: client.reportProvider,
SdkInfo: client.sdkInfo,
Spotlight: client.options.Spotlight,
})
client.Transport = &internalAsyncTransportAdapter{transport: transport}

Expand All @@ -525,7 +627,12 @@ func (client *Client) setupTelemetryProcessor() {
ratelimit.CategoryTraceMetric: telemetry.NewRingBuffer[protocol.TelemetryItem](ratelimit.CategoryTraceMetric, 10*100, telemetry.OverflowPolicyDropOldest, 100, 5*time.Second, client.reportRecorder),
}

client.telemetryProcessor = telemetry.NewProcessor(buffers, transport, client.dsn, client.sdkInfo, client.reportRecorder)
var spotlight telemetry.SpotlightSender
if client.options.Spotlight {
spotlight = newSpotlightEnvelopeSender(client.options)
}

client.telemetryProcessor = telemetry.NewProcessor(buffers, transport, client.dsn, client.sdkInfo, client.reportRecorder, spotlight)
Comment thread
cursor[bot] marked this conversation as resolved.
}

func (client *Client) setupIntegrations() {
Expand All @@ -537,6 +644,10 @@ func (client *Client) setupIntegrations() {
new(globalTagsIntegration),
}

if client.options.Spotlight {
integrations = append(integrations, new(spotlightIntegration))
}

if client.options.Integrations != nil {
integrations = client.options.Integrations(integrations)
}
Expand Down Expand Up @@ -952,6 +1063,8 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
debuglog.Println("Event dropped: telemetry buffer full or unavailable")
}
} else {
// If Spotlight is enabled, client.Transport is a *SpotlightTransport
// and already forwards to both Sentry and Spotlight.
client.Transport.SendEvent(event)
}

Expand Down
8 changes: 8 additions & 0 deletions hub_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,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 @@ -546,4 +550,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())
}
}
23 changes: 23 additions & 0 deletions integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,26 @@ 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.
// It is only installed when client.options.Spotlight is true, see
// setupIntegrations.
url := client.options.SpotlightURL
if url == "" {
url = defaultSpotlightURL
}
debuglog.Printf("Spotlight integration enabled. Events will be sent to %s", url)
}
2 changes: 1 addition & 1 deletion interfaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1317,7 +1317,7 @@ func TestProcessor_MutationAfterAdd(t *testing.T) {
),
}

proc := telemetry.NewProcessor(buffers, transport, dsn, func() *protocol.SdkInfo { return sdk }, nil)
proc := telemetry.NewProcessor(buffers, transport, dsn, func() *protocol.SdkInfo { return sdk }, nil, nil)

contexts := map[string]Context{
"app": {"version": "1.0"},
Expand Down
Loading