Skip to content

Commit 7ba9727

Browse files
committed
feat: Add Spotlight integration for local development debugging
Add SpotlightTransport that sends events to local Spotlight server for real-time debugging during development. The integration works by wrapping the existing transport and duplicating events to Spotlight. Features: - Spotlight transport decorator supporting custom URL overrides - SENTRY_SPOTLIGHT environment variable for easy enabling - Example program demonstrating usage patterns - README documentation update
1 parent 8921d3c commit 7ba9727

File tree

6 files changed

+396
-1
lines changed

6 files changed

+396
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ The SDK supports reporting errors and tracking application performance.
6464
To get started, have a look at one of our [examples](_examples/):
6565
- [Basic error instrumentation](_examples/basic/main.go)
6666
- [Error and tracing for HTTP servers](_examples/http/main.go)
67+
- [Local development debugging with Spotlight](_examples/spotlight/main.go)
6768

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

_examples/spotlight/main.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// This is an example program that demonstrates Sentry Go SDK integration
2+
// with Spotlight for local development debugging.
3+
//
4+
// Try it by running:
5+
//
6+
// go run main.go
7+
//
8+
// To actually report events to Sentry, set the DSN either by editing the
9+
// appropriate line below or setting the environment variable SENTRY_DSN to
10+
// match the DSN of your Sentry project.
11+
//
12+
// Before running this example, make sure Spotlight is running:
13+
//
14+
// npm install -g @spotlightjs/spotlight
15+
// spotlight
16+
//
17+
// Then open http://localhost:8969 in your browser to see the Spotlight UI.
18+
package main
19+
20+
import (
21+
"context"
22+
"errors"
23+
"log"
24+
"time"
25+
26+
"github.com/getsentry/sentry-go"
27+
)
28+
29+
func main() {
30+
err := sentry.Init(sentry.ClientOptions{
31+
// Either set your DSN here or set the SENTRY_DSN environment variable.
32+
Dsn: "",
33+
// Enable printing of SDK debug messages.
34+
// Useful when getting started or trying to figure something out.
35+
Debug: true,
36+
// Enable Spotlight for local debugging.
37+
Spotlight: true,
38+
// Enable tracing to see performance data in Spotlight.
39+
EnableTracing: true,
40+
TracesSampleRate: 1.0,
41+
})
42+
if err != nil {
43+
log.Fatalf("sentry.Init: %s", err)
44+
}
45+
// Flush buffered events before the program terminates.
46+
// Set the timeout to the maximum duration the program can afford to wait.
47+
defer sentry.Flush(2 * time.Second)
48+
49+
log.Println("Sending sample events to Spotlight...")
50+
51+
// Capture a simple message
52+
sentry.CaptureMessage("Hello from Spotlight!")
53+
54+
// Capture an exception
55+
sentry.CaptureException(errors.New("example error for Spotlight debugging"))
56+
57+
// Capture an event with additional context
58+
sentry.WithScope(func(scope *sentry.Scope) {
59+
scope.SetTag("environment", "development")
60+
scope.SetLevel(sentry.LevelWarning)
61+
scope.SetContext("example", map[string]interface{}{
62+
"feature": "spotlight_integration",
63+
"version": "1.0.0",
64+
})
65+
sentry.CaptureMessage("Event with additional context")
66+
})
67+
68+
// Performance monitoring example
69+
span := sentry.StartSpan(context.Background(), "example.operation")
70+
defer span.Finish()
71+
72+
span.SetData("example", "data")
73+
childSpan := span.StartChild("child.operation")
74+
// Simulate some work
75+
time.Sleep(100 * time.Millisecond)
76+
childSpan.Finish()
77+
78+
log.Println("Events sent! Check your Spotlight UI at http://localhost:8969")
79+
}

client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,13 @@ type ClientOptions struct {
243243
//
244244
// By default, this is empty and all status codes are traced.
245245
TraceIgnoreStatusCodes [][]int
246+
// Enable Spotlight for local development debugging.
247+
// When enabled, events are sent to the local Spotlight sidecar.
248+
// Default Spotlight URL is http://localhost:8969/
249+
Spotlight bool
250+
// SpotlightURL is the URL to send events to when Spotlight is enabled.
251+
// Defaults to http://localhost:8969/stream
252+
SpotlightURL string
246253
}
247254

248255
// Client is the underlying processor that is used by the main API and Hub
@@ -370,6 +377,12 @@ func NewClient(options ClientOptions) (*Client, error) {
370377
}
371378

372379
func (client *Client) setupTransport() {
380+
if !client.options.Spotlight {
381+
if spotlightEnv := os.Getenv("SENTRY_SPOTLIGHT"); spotlightEnv == "true" || spotlightEnv == "1" {
382+
client.options.Spotlight = true
383+
}
384+
}
385+
373386
opts := client.options
374387
transport := opts.Transport
375388

@@ -381,6 +394,10 @@ func (client *Client) setupTransport() {
381394
}
382395
}
383396

397+
if opts.Spotlight {
398+
transport = NewSpotlightTransport(transport)
399+
}
400+
384401
transport.Configure(opts)
385402
client.Transport = transport
386403
}
@@ -393,6 +410,7 @@ func (client *Client) setupIntegrations() {
393410
new(ignoreErrorsIntegration),
394411
new(ignoreTransactionsIntegration),
395412
new(globalTagsIntegration),
413+
new(spotlightIntegration),
396414
}
397415

398416
if client.options.Integrations != nil {

integrations.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,3 +389,30 @@ func loadEnvTags() map[string]string {
389389
}
390390
return tags
391391
}
392+
393+
// ================================
394+
// Spotlight Integration
395+
// ================================
396+
397+
type spotlightIntegration struct{}
398+
399+
func (si *spotlightIntegration) Name() string {
400+
return "Spotlight"
401+
}
402+
403+
func (si *spotlightIntegration) SetupOnce(client *Client) {
404+
// The spotlight integration doesn't add event processors.
405+
// It works by wrapping the transport in setupTransport().
406+
// This integration is mainly for completeness and debugging visibility.
407+
if client.options.Spotlight {
408+
DebugLogger.Printf("Spotlight integration enabled. Events will be sent to %s",
409+
client.getSpotlightURL())
410+
}
411+
}
412+
413+
func (client *Client) getSpotlightURL() string {
414+
if client.options.SpotlightURL != "" {
415+
return client.options.SpotlightURL
416+
}
417+
return "http://localhost:8969/stream"
418+
}

spotlight_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package sentry
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestSpotlightTransport(t *testing.T) {
12+
// Mock Spotlight server
13+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
if r.Method != "POST" {
15+
t.Errorf("Expected POST, got %s", r.Method)
16+
}
17+
if r.URL.Path != "/stream" {
18+
t.Errorf("Expected /stream, got %s", r.URL.Path)
19+
}
20+
if ct := r.Header.Get("Content-Type"); ct != "application/x-sentry-envelope" {
21+
t.Errorf("Expected application/x-sentry-envelope, got %s", ct)
22+
}
23+
if ua := r.Header.Get("User-Agent"); ua != "sentry-go/"+SDKVersion {
24+
t.Errorf("Expected sentry-go/%s, got %s", SDKVersion, ua)
25+
}
26+
w.WriteHeader(http.StatusOK)
27+
}))
28+
defer server.Close()
29+
30+
mock := &mockTransport{}
31+
st := NewSpotlightTransport(mock)
32+
st.Configure(ClientOptions{SpotlightURL: server.URL + "/stream"})
33+
34+
event := NewEvent()
35+
event.Message = "Test message"
36+
st.SendEvent(event)
37+
38+
time.Sleep(100 * time.Millisecond)
39+
40+
if len(mock.events) != 1 {
41+
t.Errorf("Expected 1 event, got %d", len(mock.events))
42+
}
43+
if mock.events[0].Message != "Test message" {
44+
t.Errorf("Expected 'Test message', got %s", mock.events[0].Message)
45+
}
46+
}
47+
48+
func TestSpotlightTransportWithNoopUnderlying(t *testing.T) {
49+
// Mock Spotlight server
50+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
51+
w.WriteHeader(http.StatusOK)
52+
}))
53+
defer server.Close()
54+
55+
st := NewSpotlightTransport(noopTransport{})
56+
st.Configure(ClientOptions{SpotlightURL: server.URL + "/stream"})
57+
58+
event := NewEvent()
59+
event.Message = "Test message"
60+
st.SendEvent(event)
61+
}
62+
63+
func TestSpotlightClientOptions(t *testing.T) {
64+
tests := []struct {
65+
name string
66+
options ClientOptions
67+
envVar string
68+
wantErr bool
69+
hasSpotlight bool
70+
}{
71+
{
72+
name: "Spotlight enabled with DSN",
73+
options: ClientOptions{
74+
Dsn: "https://[email protected]/123",
75+
Spotlight: true,
76+
},
77+
hasSpotlight: true,
78+
},
79+
{
80+
name: "Spotlight enabled without DSN",
81+
options: ClientOptions{
82+
Spotlight: true,
83+
},
84+
hasSpotlight: true,
85+
},
86+
{
87+
name: "Spotlight disabled",
88+
options: ClientOptions{
89+
Dsn: "https://[email protected]/123",
90+
},
91+
hasSpotlight: false,
92+
},
93+
{
94+
name: "Spotlight with custom URL",
95+
options: ClientOptions{
96+
Spotlight: true,
97+
SpotlightURL: "http://custom:9000/events",
98+
},
99+
hasSpotlight: true,
100+
},
101+
{
102+
name: "Spotlight enabled via env var",
103+
options: ClientOptions{
104+
Dsn: "https://[email protected]/123",
105+
},
106+
envVar: "true",
107+
hasSpotlight: true,
108+
},
109+
{
110+
name: "Spotlight enabled via env var (numeric)",
111+
options: ClientOptions{
112+
Dsn: "https://[email protected]/123",
113+
},
114+
envVar: "1",
115+
hasSpotlight: true,
116+
},
117+
{
118+
name: "Spotlight disabled via env var",
119+
options: ClientOptions{
120+
Dsn: "https://[email protected]/123",
121+
},
122+
envVar: "false",
123+
hasSpotlight: false,
124+
},
125+
}
126+
127+
for _, tt := range tests {
128+
t.Run(tt.name, func(t *testing.T) {
129+
if tt.envVar != "" {
130+
t.Setenv("SENTRY_SPOTLIGHT", tt.envVar)
131+
}
132+
133+
client, err := NewClient(tt.options)
134+
if (err != nil) != tt.wantErr {
135+
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
136+
return
137+
}
138+
139+
if err != nil {
140+
return
141+
}
142+
143+
_, isSpotlight := client.Transport.(*SpotlightTransport)
144+
if isSpotlight != tt.hasSpotlight {
145+
t.Errorf("Expected SpotlightTransport = %v, got %v", tt.hasSpotlight, isSpotlight)
146+
}
147+
})
148+
}
149+
}
150+
151+
// mockTransport is a simple transport for testing
152+
type mockTransport struct {
153+
events []*Event
154+
}
155+
156+
func (m *mockTransport) Configure(ClientOptions) {}
157+
158+
func (m *mockTransport) SendEvent(event *Event) {
159+
m.events = append(m.events, event)
160+
}
161+
162+
func (m *mockTransport) Flush(time.Duration) bool {
163+
return true
164+
}
165+
166+
func (m *mockTransport) FlushWithContext(ctx context.Context) bool {
167+
return true
168+
}
169+
170+
func (m *mockTransport) Close() {}

0 commit comments

Comments
 (0)