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
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")
}
19 changes: 19 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,13 @@ 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/
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
}
Expand Down Expand Up @@ -339,6 +346,13 @@ func NewClient(options ClientOptions) (*Client, error) {
options.TraceIgnoreStatusCodes = [][]int{{404}}
}

// Check for Spotlight environment variable
if !options.Spotlight {
if spotlightEnv := os.Getenv("SENTRY_SPOTLIGHT"); spotlightEnv == "true" || spotlightEnv == "1" {
options.Spotlight = 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 @@ -406,6 +420,10 @@ func (client *Client) setupTransport() {
}
}

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

transport.Configure(opts)
client.Transport = transport
}
Expand Down Expand Up @@ -464,6 +482,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 @@ -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"
}
154 changes: 154 additions & 0 deletions spotlight_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package sentry

import (
"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.Sdk.Name = "sentry-go"
event.Sdk.Version = SDKVersion
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)
}

if !st.Flush(time.Second) {
t.Errorf("Expected Flush to succeed")
}
}

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://user@sentry.io/123",
Spotlight: true,
},
hasSpotlight: true,
},
{
name: "Spotlight enabled without DSN",
options: ClientOptions{
Spotlight: true,
},
hasSpotlight: true,
},
{
name: "Spotlight disabled",
options: ClientOptions{
Dsn: "https://user@sentry.io/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://user@sentry.io/123",
},
envVar: "true",
hasSpotlight: true,
},
{
name: "Spotlight enabled via env var (numeric)",
options: ClientOptions{
Dsn: "https://user@sentry.io/123",
},
envVar: "1",
hasSpotlight: true,
},
{
name: "Spotlight disabled via env var",
options: ClientOptions{
Dsn: "https://user@sentry.io/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)
}
})
}
}
Loading
Loading