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 @@ -64,6 +64,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
79 changes: 79 additions & 0 deletions _examples/spotlight/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// This is an example program that demonstrates Sentry Go SDK integration
// with Spotlight for local development debugging.
//
// Try it by running:
//
// go run main.go
//
// To actually report events to Sentry, set the DSN either by editing the
// appropriate line below or setting the environment variable SENTRY_DSN to
// match the DSN of your Sentry project.
//
// 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")
}
18 changes: 18 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ type ClientOptions struct {
//
// By default, this is empty and all status codes are traced.
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/
Spotlight bool
// SpotlightURL is the URL to send events to when Spotlight is enabled.
// Defaults to http://localhost:8969/stream
SpotlightURL string
}

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

func (client *Client) setupTransport() {
if !client.options.Spotlight {
if spotlightEnv := os.Getenv("SENTRY_SPOTLIGHT"); spotlightEnv == "true" || spotlightEnv == "1" {
client.options.Spotlight = true
}
}

Comment on lines +380 to +385
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This option should be on the NewClient method.

opts := client.options
transport := opts.Transport

Expand All @@ -381,6 +394,10 @@ func (client *Client) setupTransport() {
}
}

if opts.Spotlight {
transport = NewSpotlightTransport(transport)
}

transport.Configure(opts)
client.Transport = transport
}
Expand All @@ -393,6 +410,7 @@ func (client *Client) setupIntegrations() {
new(ignoreErrorsIntegration),
new(ignoreTransactionsIntegration),
new(globalTagsIntegration),
new(spotlightIntegration),
}

if client.options.Integrations != nil {
Expand Down
27 changes: 27 additions & 0 deletions integrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,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"
}
170 changes: 170 additions & 0 deletions spotlight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package sentry

import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestSpotlightTransport(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if we can extend this test to verify that the underlying transport actually flushes and sends the events as well, so that it guards against breaking it.

// Mock Spotlight server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
t.Errorf("Expected POST, got %s", r.Method)
}
if r.URL.Path != "/stream" {
t.Errorf("Expected /stream, got %s", r.URL.Path)
}
if ct := r.Header.Get("Content-Type"); ct != "application/x-sentry-envelope" {
t.Errorf("Expected application/x-sentry-envelope, got %s", ct)
}
if ua := r.Header.Get("User-Agent"); ua != "sentry-go/"+SDKVersion {
t.Errorf("Expected sentry-go/%s, got %s", SDKVersion, ua)
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

mock := &mockTransport{}
st := NewSpotlightTransport(mock)
st.Configure(ClientOptions{SpotlightURL: server.URL + "/stream"})

event := NewEvent()
event.Message = "Test message"
st.SendEvent(event)

time.Sleep(100 * time.Millisecond)

if len(mock.events) != 1 {
t.Errorf("Expected 1 event, got %d", len(mock.events))
}
if mock.events[0].Message != "Test message" {
t.Errorf("Expected 'Test message', got %s", mock.events[0].Message)
}
}

func TestSpotlightTransportWithNoopUnderlying(_ *testing.T) {
// Mock Spotlight server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

st := NewSpotlightTransport(noopTransport{})
st.Configure(ClientOptions{SpotlightURL: server.URL + "/stream"})

event := NewEvent()
event.Message = "Test message"
st.SendEvent(event)
}

func TestSpotlightClientOptions(t *testing.T) {
tests := []struct {
name string
options ClientOptions
envVar string
wantErr bool
hasSpotlight bool
}{
{
name: "Spotlight enabled with DSN",
options: ClientOptions{
Dsn: "https://[email protected]/123",
Spotlight: true,
},
hasSpotlight: true,
},
{
name: "Spotlight enabled without DSN",
options: ClientOptions{
Spotlight: true,
},
hasSpotlight: true,
},
{
name: "Spotlight disabled",
options: ClientOptions{
Dsn: "https://[email protected]/123",
},
hasSpotlight: false,
},
{
name: "Spotlight with custom URL",
options: ClientOptions{
Spotlight: true,
SpotlightURL: "http://custom:9000/events",
},
hasSpotlight: true,
},
{
name: "Spotlight enabled via env var",
options: ClientOptions{
Dsn: "https://[email protected]/123",
},
envVar: "true",
hasSpotlight: true,
},
{
name: "Spotlight enabled via env var (numeric)",
options: ClientOptions{
Dsn: "https://[email protected]/123",
},
envVar: "1",
hasSpotlight: true,
},
{
name: "Spotlight disabled via env var",
options: ClientOptions{
Dsn: "https://[email protected]/123",
},
envVar: "false",
hasSpotlight: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envVar != "" {
t.Setenv("SENTRY_SPOTLIGHT", tt.envVar)
}

client, err := NewClient(tt.options)
if (err != nil) != tt.wantErr {
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
return
}

if err != nil {
return
}

_, isSpotlight := client.Transport.(*SpotlightTransport)
if isSpotlight != tt.hasSpotlight {
t.Errorf("Expected SpotlightTransport = %v, got %v", tt.hasSpotlight, isSpotlight)
}
})
}
}

// mockTransport is a simple transport for testing.
type mockTransport struct {
events []*Event
}

func (m *mockTransport) Configure(ClientOptions) {}

func (m *mockTransport) SendEvent(event *Event) {
m.events = append(m.events, event)
}

func (m *mockTransport) Flush(time.Duration) bool {
return true
}

func (m *mockTransport) FlushWithContext(_ context.Context) bool {
return true
}

func (m *mockTransport) Close() {}
Comment on lines +151 to +170
Copy link
Contributor

Choose a reason for hiding this comment

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

we already have a MockTransport instance that can be used for tests.

Loading
Loading