Implement IronTelemetry Go SDK
- Core client with exception/message capture - Journey and step tracking with breadcrumb correlation - Breadcrumb management with ring buffer - HTTP transport with context support - Full stack trace capture for exceptions - Thread-safe operations with mutex protection - Sample rate and beforeSend filtering - Tags, extras, and user context
This commit is contained in:
parent
49c96bdedc
commit
09626831a2
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Binaries
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
debug
|
||||||
|
*.log
|
||||||
310
README.md
310
README.md
|
|
@ -1,2 +1,308 @@
|
||||||
# irontelemetry-go
|
# IronTelemetry SDK for Go
|
||||||
IronTelemetry SDK for Go - Error monitoring and crash reporting
|
|
||||||
|
Error monitoring and crash reporting SDK for Go applications. Capture exceptions, track user journeys, and get insights to fix issues faster.
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/github.com/IronServices/irontelemetry-go)
|
||||||
|
[](https://goreportcard.com/report/github.com/IronServices/irontelemetry-go)
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/IronServices/irontelemetry-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Basic Exception Capture
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
irontelemetry "github.com/IronServices/irontelemetry-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize with your DSN
|
||||||
|
client, err := irontelemetry.New(irontelemetry.Options{
|
||||||
|
DSN: "https://pk_live_xxx@irontelemetry.com",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Capture exceptions
|
||||||
|
if err := doSomething(); err != nil {
|
||||||
|
client.CaptureException(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doSomething() error {
|
||||||
|
return errors.New("something went wrong")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Journey Tracking
|
||||||
|
|
||||||
|
Track user journeys to understand the context of errors:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
irontelemetry "github.com/IronServices/irontelemetry-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client, _ := irontelemetry.New(irontelemetry.Options{
|
||||||
|
DSN: "https://pk_live_xxx@irontelemetry.com",
|
||||||
|
})
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
// Start a journey
|
||||||
|
journey := client.StartJourney("Checkout Flow")
|
||||||
|
journey.SetUser("user-123", "user@example.com", "John Doe")
|
||||||
|
|
||||||
|
// Track steps
|
||||||
|
step := journey.StartStep("Validate Cart", irontelemetry.CategoryBusiness)
|
||||||
|
if err := validateCart(); err != nil {
|
||||||
|
step.Fail(err)
|
||||||
|
journey.Fail(err)
|
||||||
|
client.CaptureException(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step.Complete()
|
||||||
|
|
||||||
|
step = journey.StartStep("Process Payment", irontelemetry.CategoryBusiness)
|
||||||
|
if err := processPayment(); err != nil {
|
||||||
|
step.Fail(err)
|
||||||
|
journey.Fail(err)
|
||||||
|
client.CaptureException(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step.Complete()
|
||||||
|
|
||||||
|
journey.Complete()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the helper method:
|
||||||
|
|
||||||
|
```go
|
||||||
|
journey := client.StartJourney("Checkout Flow")
|
||||||
|
|
||||||
|
err := journey.RunStep("Validate Cart", irontelemetry.CategoryBusiness, func() error {
|
||||||
|
return validateCart()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
journey.Fail(err)
|
||||||
|
client.CaptureException(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = journey.RunStep("Process Payment", irontelemetry.CategoryBusiness, func() error {
|
||||||
|
return processPayment()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
journey.Fail(err)
|
||||||
|
client.CaptureException(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
journey.Complete()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
client, err := irontelemetry.New(irontelemetry.Options{
|
||||||
|
DSN: "https://pk_live_xxx@irontelemetry.com",
|
||||||
|
Environment: "production",
|
||||||
|
AppVersion: "1.2.3",
|
||||||
|
SampleRate: 1.0, // 100% of events
|
||||||
|
Debug: false,
|
||||||
|
BeforeSend: func(event *irontelemetry.TelemetryEvent) *irontelemetry.TelemetryEvent {
|
||||||
|
// Filter or modify events
|
||||||
|
if strings.Contains(event.Message, "expected") {
|
||||||
|
return nil // Drop the event
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `DSN` | string | required | Your Data Source Name |
|
||||||
|
| `Environment` | string | "production" | Environment name |
|
||||||
|
| `AppVersion` | string | "0.0.0" | Application version |
|
||||||
|
| `SampleRate` | float64 | 1.0 | Sample rate (0.0 to 1.0) |
|
||||||
|
| `MaxBreadcrumbs` | int | 100 | Max breadcrumbs to keep |
|
||||||
|
| `Debug` | bool | false | Enable debug logging |
|
||||||
|
| `BeforeSend` | func | nil | Hook to filter/modify events |
|
||||||
|
| `EnableOfflineQueue` | bool | true | Enable offline queue |
|
||||||
|
| `MaxOfflineQueueSize` | int | 500 | Max offline queue size |
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Stack Traces**: Full stack traces captured with every exception
|
||||||
|
- **Journey Tracking**: Track user flows and correlate errors with context
|
||||||
|
- **Breadcrumbs**: Leave a trail of events leading up to an error
|
||||||
|
- **User Context**: Associate errors with specific users
|
||||||
|
- **Tags & Extras**: Add custom metadata to your events
|
||||||
|
- **Context Support**: Full context.Context support for cancellation
|
||||||
|
- **Thread-Safe**: All operations are safe for concurrent use
|
||||||
|
|
||||||
|
## Breadcrumbs
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Add simple breadcrumbs
|
||||||
|
client.AddBreadcrumb("User clicked checkout button", irontelemetry.CategoryUI)
|
||||||
|
client.AddBreadcrumb("Payment API called", irontelemetry.CategoryHTTP)
|
||||||
|
|
||||||
|
// With level
|
||||||
|
client.AddBreadcrumbWithLevel(
|
||||||
|
"User logged in",
|
||||||
|
irontelemetry.CategoryAuth,
|
||||||
|
irontelemetry.SeverityInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
// With data
|
||||||
|
client.AddBreadcrumbWithData(
|
||||||
|
"API request completed",
|
||||||
|
irontelemetry.CategoryHTTP,
|
||||||
|
irontelemetry.SeverityInfo,
|
||||||
|
map[string]any{
|
||||||
|
"url": "/api/checkout",
|
||||||
|
"statusCode": 200,
|
||||||
|
"duration": 150,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breadcrumb Categories
|
||||||
|
|
||||||
|
```go
|
||||||
|
irontelemetry.CategoryUI // User interface interactions
|
||||||
|
irontelemetry.CategoryHTTP // HTTP requests
|
||||||
|
irontelemetry.CategoryNavigation // Page/route navigation
|
||||||
|
irontelemetry.CategoryConsole // Console output
|
||||||
|
irontelemetry.CategoryAuth // Authentication events
|
||||||
|
irontelemetry.CategoryBusiness // Business logic events
|
||||||
|
irontelemetry.CategoryNotification // Notification events
|
||||||
|
irontelemetry.CategoryCustom // Custom events
|
||||||
|
```
|
||||||
|
|
||||||
|
## Severity Levels
|
||||||
|
|
||||||
|
```go
|
||||||
|
irontelemetry.SeverityDebug
|
||||||
|
irontelemetry.SeverityInfo
|
||||||
|
irontelemetry.SeverityWarning
|
||||||
|
irontelemetry.SeverityError
|
||||||
|
irontelemetry.SeverityFatal
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Context
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple user ID
|
||||||
|
client.SetUserByID("user-123")
|
||||||
|
|
||||||
|
// With email
|
||||||
|
client.SetUserWithEmail("user-123", "user@example.com")
|
||||||
|
|
||||||
|
// Full user object
|
||||||
|
client.SetUser(&irontelemetry.User{
|
||||||
|
ID: "user-123",
|
||||||
|
Email: "user@example.com",
|
||||||
|
Name: "John Doe",
|
||||||
|
Data: map[string]any{
|
||||||
|
"plan": "premium",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tags and Extra Data
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Set individual tags
|
||||||
|
client.SetTag("release", "v1.2.3")
|
||||||
|
client.SetTag("server", "prod-1")
|
||||||
|
|
||||||
|
// Set multiple tags
|
||||||
|
client.SetTags(map[string]string{
|
||||||
|
"release": "v1.2.3",
|
||||||
|
"server": "prod-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set extra data
|
||||||
|
client.SetExtra("request_id", "abc-123")
|
||||||
|
client.SetExtras(map[string]any{
|
||||||
|
"request_id": "abc-123",
|
||||||
|
"user_agent": "Mozilla/5.0...",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context Support
|
||||||
|
|
||||||
|
All capture methods support context for cancellation:
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result := client.CaptureExceptionWithContext(ctx, err)
|
||||||
|
if !result.Success {
|
||||||
|
log.Printf("Failed to send event: %s", result.Error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## HTTP Middleware Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TelemetryMiddleware(client *irontelemetry.Client) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
client.AddBreadcrumbWithData(
|
||||||
|
"HTTP Request",
|
||||||
|
irontelemetry.CategoryHTTP,
|
||||||
|
irontelemetry.SeverityInfo,
|
||||||
|
map[string]any{
|
||||||
|
"method": r.Method,
|
||||||
|
"url": r.URL.String(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
client.CaptureException(fmt.Errorf("panic: %v", err))
|
||||||
|
panic(err) // Re-panic
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](https://www.irontelemetry.com/docs)
|
||||||
|
- [Dashboard](https://www.irontelemetry.com)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BreadcrumbManager manages a ring buffer of breadcrumbs
|
||||||
|
type BreadcrumbManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
breadcrumbs []Breadcrumb
|
||||||
|
maxBreadcrumbs int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBreadcrumbManager creates a new BreadcrumbManager
|
||||||
|
func NewBreadcrumbManager(maxBreadcrumbs int) *BreadcrumbManager {
|
||||||
|
return &BreadcrumbManager{
|
||||||
|
breadcrumbs: make([]Breadcrumb, 0, maxBreadcrumbs),
|
||||||
|
maxBreadcrumbs: maxBreadcrumbs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a breadcrumb to the ring buffer
|
||||||
|
func (bm *BreadcrumbManager) Add(breadcrumb Breadcrumb) {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
|
||||||
|
if breadcrumb.Timestamp.IsZero() {
|
||||||
|
breadcrumb.Timestamp = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bm.breadcrumbs) >= bm.maxBreadcrumbs {
|
||||||
|
// Shift elements to make room (ring buffer behavior)
|
||||||
|
copy(bm.breadcrumbs, bm.breadcrumbs[1:])
|
||||||
|
bm.breadcrumbs = bm.breadcrumbs[:len(bm.breadcrumbs)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
bm.breadcrumbs = append(bm.breadcrumbs, breadcrumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSimple adds a simple breadcrumb with just a message and category
|
||||||
|
func (bm *BreadcrumbManager) AddSimple(message string, category BreadcrumbCategory) {
|
||||||
|
bm.Add(Breadcrumb{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Category: category,
|
||||||
|
Message: message,
|
||||||
|
Level: SeverityInfo,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWithLevel adds a breadcrumb with a specific level
|
||||||
|
func (bm *BreadcrumbManager) AddWithLevel(message string, category BreadcrumbCategory, level SeverityLevel) {
|
||||||
|
bm.Add(Breadcrumb{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Category: category,
|
||||||
|
Message: message,
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWithData adds a breadcrumb with additional data
|
||||||
|
func (bm *BreadcrumbManager) AddWithData(message string, category BreadcrumbCategory, level SeverityLevel, data map[string]any) {
|
||||||
|
bm.Add(Breadcrumb{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Category: category,
|
||||||
|
Message: message,
|
||||||
|
Level: level,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll returns a copy of all breadcrumbs
|
||||||
|
func (bm *BreadcrumbManager) GetAll() []Breadcrumb {
|
||||||
|
bm.mu.RLock()
|
||||||
|
defer bm.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]Breadcrumb, len(bm.breadcrumbs))
|
||||||
|
copy(result, bm.breadcrumbs)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all breadcrumbs
|
||||||
|
func (bm *BreadcrumbManager) Clear() {
|
||||||
|
bm.mu.Lock()
|
||||||
|
defer bm.mu.Unlock()
|
||||||
|
|
||||||
|
bm.breadcrumbs = make([]Breadcrumb, 0, bm.maxBreadcrumbs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of breadcrumbs
|
||||||
|
func (bm *BreadcrumbManager) Count() int {
|
||||||
|
bm.mu.RLock()
|
||||||
|
defer bm.mu.RUnlock()
|
||||||
|
|
||||||
|
return len(bm.breadcrumbs)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the main IronTelemetry client
|
||||||
|
type Client struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
opts Options
|
||||||
|
parsedDSN *ParsedDSN
|
||||||
|
transport *Transport
|
||||||
|
breadcrumbs *BreadcrumbManager
|
||||||
|
journeys *JourneyManager
|
||||||
|
user *User
|
||||||
|
tags map[string]string
|
||||||
|
extra map[string]any
|
||||||
|
initialized bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new IronTelemetry client
|
||||||
|
func New(opts Options) (*Client, error) {
|
||||||
|
resolvedOpts, parsedDSN, err := ResolveOptions(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &Client{
|
||||||
|
opts: resolvedOpts,
|
||||||
|
parsedDSN: parsedDSN,
|
||||||
|
transport: NewTransport(parsedDSN, resolvedOpts.APIBaseURL, resolvedOpts.Debug),
|
||||||
|
breadcrumbs: NewBreadcrumbManager(resolvedOpts.MaxBreadcrumbs),
|
||||||
|
tags: make(map[string]string),
|
||||||
|
extra: make(map[string]any),
|
||||||
|
initialized: true,
|
||||||
|
}
|
||||||
|
client.journeys = NewJourneyManager(client)
|
||||||
|
|
||||||
|
if opts.Debug {
|
||||||
|
fmt.Printf("[IronTelemetry] Initialized with DSN: %s\n", parsedDSN.APIBaseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureException captures an error and sends it to the server
|
||||||
|
func (c *Client) CaptureException(err error) SendResult {
|
||||||
|
return c.CaptureExceptionWithContext(context.Background(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureExceptionWithContext captures an error with context
|
||||||
|
func (c *Client) CaptureExceptionWithContext(ctx context.Context, err error) SendResult {
|
||||||
|
if err == nil {
|
||||||
|
return SendResult{Success: false, Error: "nil error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
event := c.createEvent(SeverityError, err.Error())
|
||||||
|
event.Exception = &ExceptionInfo{
|
||||||
|
Type: fmt.Sprintf("%T", err),
|
||||||
|
Message: err.Error(),
|
||||||
|
Stacktrace: captureStacktrace(3),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendEvent(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureMessage captures a message and sends it to the server
|
||||||
|
func (c *Client) CaptureMessage(message string, level SeverityLevel) SendResult {
|
||||||
|
return c.CaptureMessageWithContext(context.Background(), message, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CaptureMessageWithContext captures a message with context
|
||||||
|
func (c *Client) CaptureMessageWithContext(ctx context.Context, message string, level SeverityLevel) SendResult {
|
||||||
|
event := c.createEvent(level, message)
|
||||||
|
return c.sendEvent(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumb adds a breadcrumb
|
||||||
|
func (c *Client) AddBreadcrumb(message string, category BreadcrumbCategory) {
|
||||||
|
c.breadcrumbs.AddSimple(message, category)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumbWithLevel adds a breadcrumb with a level
|
||||||
|
func (c *Client) AddBreadcrumbWithLevel(message string, category BreadcrumbCategory, level SeverityLevel) {
|
||||||
|
c.breadcrumbs.AddWithLevel(message, category, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddBreadcrumbWithData adds a breadcrumb with data
|
||||||
|
func (c *Client) AddBreadcrumbWithData(message string, category BreadcrumbCategory, level SeverityLevel, data map[string]any) {
|
||||||
|
c.breadcrumbs.AddWithData(message, category, level, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser sets the user context
|
||||||
|
func (c *Client) SetUser(user *User) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.user = user
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserByID sets the user context by ID
|
||||||
|
func (c *Client) SetUserByID(id string) {
|
||||||
|
c.SetUser(&User{ID: id})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserWithEmail sets the user context with ID and email
|
||||||
|
func (c *Client) SetUserWithEmail(id, email string) {
|
||||||
|
c.SetUser(&User{ID: id, Email: email})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTag sets a tag
|
||||||
|
func (c *Client) SetTag(key, value string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.tags[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTags sets multiple tags
|
||||||
|
func (c *Client) SetTags(tags map[string]string) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for k, v := range tags {
|
||||||
|
c.tags[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtra sets extra data
|
||||||
|
func (c *Client) SetExtra(key string, value any) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.extra[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExtras sets multiple extra data values
|
||||||
|
func (c *Client) SetExtras(extras map[string]any) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for k, v := range extras {
|
||||||
|
c.extra[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartJourney starts a new journey
|
||||||
|
func (c *Client) StartJourney(name string) *Journey {
|
||||||
|
return c.journeys.StartJourney(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentJourney returns the current journey context
|
||||||
|
func (c *Client) GetCurrentJourney() *JourneyContext {
|
||||||
|
return c.journeys.GetCurrent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearBreadcrumbs clears all breadcrumbs
|
||||||
|
func (c *Client) ClearBreadcrumbs() {
|
||||||
|
c.breadcrumbs.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush flushes any pending events (placeholder for offline queue)
|
||||||
|
func (c *Client) Flush() {
|
||||||
|
// Future: Implement offline queue flushing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the client and flushes pending events
|
||||||
|
func (c *Client) Close() {
|
||||||
|
c.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) createEvent(level SeverityLevel, message string) *TelemetryEvent {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Copy tags
|
||||||
|
tags := make(map[string]string, len(c.tags))
|
||||||
|
for k, v := range c.tags {
|
||||||
|
tags[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy extra
|
||||||
|
extra := make(map[string]any, len(c.extra))
|
||||||
|
for k, v := range c.extra {
|
||||||
|
extra[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
event := &TelemetryEvent{
|
||||||
|
EventID: GenerateEventID(),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
Tags: tags,
|
||||||
|
Extra: extra,
|
||||||
|
Breadcrumbs: c.breadcrumbs.GetAll(),
|
||||||
|
Environment: c.opts.Environment,
|
||||||
|
AppVersion: c.opts.AppVersion,
|
||||||
|
Platform: PlatformInfo{
|
||||||
|
Name: "go",
|
||||||
|
Version: runtime.Version(),
|
||||||
|
OS: runtime.GOOS + "/" + runtime.GOARCH,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy user if set
|
||||||
|
if c.user != nil {
|
||||||
|
event.User = &User{
|
||||||
|
ID: c.user.ID,
|
||||||
|
Email: c.user.Email,
|
||||||
|
Name: c.user.Name,
|
||||||
|
}
|
||||||
|
if c.user.Data != nil {
|
||||||
|
event.User.Data = make(map[string]any, len(c.user.Data))
|
||||||
|
for k, v := range c.user.Data {
|
||||||
|
event.User.Data[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy journey if active
|
||||||
|
if journey := c.journeys.GetCurrent(); journey != nil {
|
||||||
|
event.Journey = &JourneyContext{
|
||||||
|
JourneyID: journey.JourneyID,
|
||||||
|
Name: journey.Name,
|
||||||
|
CurrentStep: journey.CurrentStep,
|
||||||
|
StartedAt: journey.StartedAt,
|
||||||
|
Metadata: make(map[string]any, len(journey.Metadata)),
|
||||||
|
}
|
||||||
|
for k, v := range journey.Metadata {
|
||||||
|
event.Journey.Metadata[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendEvent(ctx context.Context, event *TelemetryEvent) SendResult {
|
||||||
|
// Check sample rate
|
||||||
|
if c.opts.SampleRate < 1.0 && rand.Float64() > c.opts.SampleRate {
|
||||||
|
if c.opts.Debug {
|
||||||
|
fmt.Printf("[IronTelemetry] Event sampled out: %s\n", event.EventID)
|
||||||
|
}
|
||||||
|
return SendResult{Success: true, EventID: event.EventID}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply beforeSend hook
|
||||||
|
if c.opts.BeforeSend != nil {
|
||||||
|
event = c.opts.BeforeSend(event)
|
||||||
|
if event == nil {
|
||||||
|
if c.opts.Debug {
|
||||||
|
fmt.Println("[IronTelemetry] Event dropped by beforeSend hook")
|
||||||
|
}
|
||||||
|
return SendResult{Success: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.transport.Send(ctx, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureStacktrace(skip int) []StackFrame {
|
||||||
|
var frames []StackFrame
|
||||||
|
pcs := make([]uintptr, 32)
|
||||||
|
n := runtime.Callers(skip, pcs)
|
||||||
|
if n == 0 {
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
pcs = pcs[:n]
|
||||||
|
runtimeFrames := runtime.CallersFrames(pcs)
|
||||||
|
|
||||||
|
for {
|
||||||
|
frame, more := runtimeFrames.Next()
|
||||||
|
frames = append(frames, StackFrame{
|
||||||
|
Function: frame.Function,
|
||||||
|
Filename: frame.File,
|
||||||
|
Lineno: frame.Line,
|
||||||
|
})
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseDSN parses a DSN string into its components
|
||||||
|
// Format: https://pk_live_xxx@irontelemetry.com
|
||||||
|
func ParseDSN(dsn string) (*ParsedDSN, error) {
|
||||||
|
parsed, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid DSN format: " + dsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey := parsed.User.Username()
|
||||||
|
if publicKey == "" || !strings.HasPrefix(publicKey, "pk_") {
|
||||||
|
return nil, errors.New("DSN must contain a valid public key starting with pk_")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedDSN{
|
||||||
|
PublicKey: publicKey,
|
||||||
|
Host: parsed.Host,
|
||||||
|
Protocol: parsed.Scheme,
|
||||||
|
APIBaseURL: parsed.Scheme + "://" + parsed.Host,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateEventID generates a unique event ID
|
||||||
|
func GenerateEventID() string {
|
||||||
|
return uuid.New().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveOptions validates and fills in defaults for options
|
||||||
|
func ResolveOptions(opts Options) (Options, *ParsedDSN, error) {
|
||||||
|
parsedDSN, err := ParseDSN(opts.DSN)
|
||||||
|
if err != nil {
|
||||||
|
return opts, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults := DefaultOptions()
|
||||||
|
|
||||||
|
if opts.Environment == "" {
|
||||||
|
opts.Environment = defaults.Environment
|
||||||
|
}
|
||||||
|
if opts.AppVersion == "" {
|
||||||
|
opts.AppVersion = defaults.AppVersion
|
||||||
|
}
|
||||||
|
if opts.SampleRate == 0 {
|
||||||
|
opts.SampleRate = defaults.SampleRate
|
||||||
|
}
|
||||||
|
if opts.SampleRate < 0 {
|
||||||
|
opts.SampleRate = 0
|
||||||
|
}
|
||||||
|
if opts.SampleRate > 1 {
|
||||||
|
opts.SampleRate = 1
|
||||||
|
}
|
||||||
|
if opts.MaxBreadcrumbs == 0 {
|
||||||
|
opts.MaxBreadcrumbs = defaults.MaxBreadcrumbs
|
||||||
|
}
|
||||||
|
if opts.MaxOfflineQueueSize == 0 {
|
||||||
|
opts.MaxOfflineQueueSize = defaults.MaxOfflineQueueSize
|
||||||
|
}
|
||||||
|
if opts.APIBaseURL == "" {
|
||||||
|
opts.APIBaseURL = parsedDSN.APIBaseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, parsedDSN, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
module github.com/IronServices/irontelemetry-go
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.5.0
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JourneyManager manages journey context
|
||||||
|
type JourneyManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
current *JourneyContext
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJourneyManager creates a new JourneyManager
|
||||||
|
func NewJourneyManager(client *Client) *JourneyManager {
|
||||||
|
return &JourneyManager{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartJourney starts a new journey
|
||||||
|
func (jm *JourneyManager) StartJourney(name string) *Journey {
|
||||||
|
jm.mu.Lock()
|
||||||
|
defer jm.mu.Unlock()
|
||||||
|
|
||||||
|
ctx := &JourneyContext{
|
||||||
|
JourneyID: uuid.New().String(),
|
||||||
|
Name: name,
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
Metadata: make(map[string]any),
|
||||||
|
}
|
||||||
|
jm.current = ctx
|
||||||
|
|
||||||
|
// Add breadcrumb for journey start
|
||||||
|
if jm.client != nil {
|
||||||
|
jm.client.AddBreadcrumb("Started journey: "+name, CategoryBusiness)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Journey{
|
||||||
|
manager: jm,
|
||||||
|
context: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrent returns the current journey context
|
||||||
|
func (jm *JourneyManager) GetCurrent() *JourneyContext {
|
||||||
|
jm.mu.RLock()
|
||||||
|
defer jm.mu.RUnlock()
|
||||||
|
|
||||||
|
return jm.current
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears the current journey
|
||||||
|
func (jm *JourneyManager) Clear() {
|
||||||
|
jm.mu.Lock()
|
||||||
|
defer jm.mu.Unlock()
|
||||||
|
|
||||||
|
jm.current = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Journey represents an active journey
|
||||||
|
type Journey struct {
|
||||||
|
manager *JourneyManager
|
||||||
|
context *JourneyContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUser sets the user for the journey
|
||||||
|
func (j *Journey) SetUser(id, email, name string) *Journey {
|
||||||
|
j.manager.mu.Lock()
|
||||||
|
defer j.manager.mu.Unlock()
|
||||||
|
|
||||||
|
j.context.Metadata["userId"] = id
|
||||||
|
if email != "" {
|
||||||
|
j.context.Metadata["userEmail"] = email
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
j.context.Metadata["userName"] = name
|
||||||
|
}
|
||||||
|
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMetadata sets metadata for the journey
|
||||||
|
func (j *Journey) SetMetadata(key string, value any) *Journey {
|
||||||
|
j.manager.mu.Lock()
|
||||||
|
defer j.manager.mu.Unlock()
|
||||||
|
|
||||||
|
j.context.Metadata[key] = value
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartStep starts a new step in the journey
|
||||||
|
func (j *Journey) StartStep(name string, category BreadcrumbCategory) *Step {
|
||||||
|
j.manager.mu.Lock()
|
||||||
|
j.context.CurrentStep = name
|
||||||
|
j.manager.mu.Unlock()
|
||||||
|
|
||||||
|
// Add breadcrumb for step start
|
||||||
|
if j.manager.client != nil {
|
||||||
|
j.manager.client.AddBreadcrumbWithData(
|
||||||
|
"Started step: "+name,
|
||||||
|
category,
|
||||||
|
SeverityInfo,
|
||||||
|
map[string]any{
|
||||||
|
"journeyId": j.context.JourneyID,
|
||||||
|
"journeyName": j.context.Name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Step{
|
||||||
|
journey: j,
|
||||||
|
name: name,
|
||||||
|
category: category,
|
||||||
|
startedAt: time.Now(),
|
||||||
|
data: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes the journey successfully
|
||||||
|
func (j *Journey) Complete() {
|
||||||
|
if j.manager.client != nil {
|
||||||
|
j.manager.client.AddBreadcrumbWithData(
|
||||||
|
"Completed journey: "+j.context.Name,
|
||||||
|
CategoryBusiness,
|
||||||
|
SeverityInfo,
|
||||||
|
map[string]any{
|
||||||
|
"journeyId": j.context.JourneyID,
|
||||||
|
"duration": time.Since(j.context.StartedAt).Milliseconds(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
j.manager.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail marks the journey as failed
|
||||||
|
func (j *Journey) Fail(err error) {
|
||||||
|
if j.manager.client != nil {
|
||||||
|
data := map[string]any{
|
||||||
|
"journeyId": j.context.JourneyID,
|
||||||
|
"duration": time.Since(j.context.StartedAt).Milliseconds(),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
data["error"] = err.Error()
|
||||||
|
}
|
||||||
|
j.manager.client.AddBreadcrumbWithData(
|
||||||
|
"Failed journey: "+j.context.Name,
|
||||||
|
CategoryBusiness,
|
||||||
|
SeverityError,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
j.manager.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step represents a step within a journey
|
||||||
|
type Step struct {
|
||||||
|
journey *Journey
|
||||||
|
name string
|
||||||
|
category BreadcrumbCategory
|
||||||
|
startedAt time.Time
|
||||||
|
data map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetData sets data for the step
|
||||||
|
func (s *Step) SetData(key string, value any) *Step {
|
||||||
|
s.data[key] = value
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete completes the step successfully
|
||||||
|
func (s *Step) Complete() {
|
||||||
|
if s.journey.manager.client != nil {
|
||||||
|
data := map[string]any{
|
||||||
|
"journeyId": s.journey.context.JourneyID,
|
||||||
|
"journeyName": s.journey.context.Name,
|
||||||
|
"duration": time.Since(s.startedAt).Milliseconds(),
|
||||||
|
}
|
||||||
|
for k, v := range s.data {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
s.journey.manager.client.AddBreadcrumbWithData(
|
||||||
|
"Completed step: "+s.name,
|
||||||
|
s.category,
|
||||||
|
SeverityInfo,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail marks the step as failed
|
||||||
|
func (s *Step) Fail(err error) {
|
||||||
|
if s.journey.manager.client != nil {
|
||||||
|
data := map[string]any{
|
||||||
|
"journeyId": s.journey.context.JourneyID,
|
||||||
|
"journeyName": s.journey.context.Name,
|
||||||
|
"duration": time.Since(s.startedAt).Milliseconds(),
|
||||||
|
}
|
||||||
|
for k, v := range s.data {
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
data["error"] = err.Error()
|
||||||
|
}
|
||||||
|
s.journey.manager.client.AddBreadcrumbWithData(
|
||||||
|
"Failed step: "+s.name,
|
||||||
|
s.category,
|
||||||
|
SeverityError,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunStep is a helper that runs a function as a step
|
||||||
|
func (j *Journey) RunStep(name string, category BreadcrumbCategory, fn func() error) error {
|
||||||
|
step := j.StartStep(name, category)
|
||||||
|
err := fn()
|
||||||
|
if err != nil {
|
||||||
|
step.Fail(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
step.Complete()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transport handles HTTP communication with the server
|
||||||
|
type Transport struct {
|
||||||
|
apiBaseURL string
|
||||||
|
publicKey string
|
||||||
|
debug bool
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransport creates a new Transport
|
||||||
|
func NewTransport(parsedDSN *ParsedDSN, apiBaseURL string, debug bool) *Transport {
|
||||||
|
return &Transport{
|
||||||
|
apiBaseURL: apiBaseURL,
|
||||||
|
publicKey: parsedDSN.PublicKey,
|
||||||
|
debug: debug,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends an event to the server
|
||||||
|
func (t *Transport) Send(ctx context.Context, event *TelemetryEvent) SendResult {
|
||||||
|
url := t.apiBaseURL + "/api/v1/events"
|
||||||
|
|
||||||
|
body, err := json.Marshal(t.serializeEvent(event))
|
||||||
|
if err != nil {
|
||||||
|
return SendResult{Success: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return SendResult{Success: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Public-Key", t.publicKey)
|
||||||
|
|
||||||
|
resp, err := t.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
if t.debug {
|
||||||
|
fmt.Printf("[IronTelemetry] Failed to send event: %v\n", err)
|
||||||
|
}
|
||||||
|
return SendResult{Success: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
errMsg := fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
if t.debug {
|
||||||
|
fmt.Printf("[IronTelemetry] Failed to send event: %s\n", errMsg)
|
||||||
|
}
|
||||||
|
return SendResult{Success: false, Error: errMsg}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||||
|
// Even if we can't decode, the request succeeded
|
||||||
|
result.EventID = event.EventID
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.debug {
|
||||||
|
fmt.Printf("[IronTelemetry] Event sent successfully: %s\n", event.EventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SendResult{
|
||||||
|
Success: true,
|
||||||
|
EventID: result.EventID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsOnline checks if the server is reachable
|
||||||
|
func (t *Transport) IsOnline(ctx context.Context) bool {
|
||||||
|
url := t.apiBaseURL + "/api/v1/health"
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-Public-Key", t.publicKey)
|
||||||
|
|
||||||
|
resp, err := t.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return resp.StatusCode == 200
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) serializeEvent(event *TelemetryEvent) map[string]any {
|
||||||
|
breadcrumbs := make([]map[string]any, len(event.Breadcrumbs))
|
||||||
|
for i, b := range event.Breadcrumbs {
|
||||||
|
breadcrumbs[i] = map[string]any{
|
||||||
|
"timestamp": b.Timestamp.Format(time.RFC3339),
|
||||||
|
"category": b.Category,
|
||||||
|
"message": b.Message,
|
||||||
|
"level": b.Level,
|
||||||
|
"data": b.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]any{
|
||||||
|
"eventId": event.EventID,
|
||||||
|
"timestamp": event.Timestamp.Format(time.RFC3339),
|
||||||
|
"level": event.Level,
|
||||||
|
"message": event.Message,
|
||||||
|
"tags": event.Tags,
|
||||||
|
"extra": event.Extra,
|
||||||
|
"breadcrumbs": breadcrumbs,
|
||||||
|
"environment": event.Environment,
|
||||||
|
"appVersion": event.AppVersion,
|
||||||
|
"platform": map[string]any{
|
||||||
|
"name": event.Platform.Name,
|
||||||
|
"version": event.Platform.Version,
|
||||||
|
"os": event.Platform.OS,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Exception != nil {
|
||||||
|
stacktrace := make([]map[string]any, len(event.Exception.Stacktrace))
|
||||||
|
for i, f := range event.Exception.Stacktrace {
|
||||||
|
stacktrace[i] = map[string]any{
|
||||||
|
"function": f.Function,
|
||||||
|
"filename": f.Filename,
|
||||||
|
"lineno": f.Lineno,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result["exception"] = map[string]any{
|
||||||
|
"type": event.Exception.Type,
|
||||||
|
"message": event.Exception.Message,
|
||||||
|
"stacktrace": stacktrace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.User != nil {
|
||||||
|
result["user"] = map[string]any{
|
||||||
|
"id": event.User.ID,
|
||||||
|
"email": event.User.Email,
|
||||||
|
"name": event.User.Name,
|
||||||
|
"data": event.User.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Journey != nil {
|
||||||
|
result["journey"] = map[string]any{
|
||||||
|
"journeyId": event.Journey.JourneyID,
|
||||||
|
"name": event.Journey.Name,
|
||||||
|
"currentStep": event.Journey.CurrentStep,
|
||||||
|
"startedAt": event.Journey.StartedAt.Format(time.RFC3339),
|
||||||
|
"metadata": event.Journey.Metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
package irontelemetry
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SeverityLevel represents the severity of an event
|
||||||
|
type SeverityLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
SeverityDebug SeverityLevel = "debug"
|
||||||
|
SeverityInfo SeverityLevel = "info"
|
||||||
|
SeverityWarning SeverityLevel = "warning"
|
||||||
|
SeverityError SeverityLevel = "error"
|
||||||
|
SeverityFatal SeverityLevel = "fatal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BreadcrumbCategory represents the category of a breadcrumb
|
||||||
|
type BreadcrumbCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CategoryUI BreadcrumbCategory = "ui"
|
||||||
|
CategoryHTTP BreadcrumbCategory = "http"
|
||||||
|
CategoryNavigation BreadcrumbCategory = "navigation"
|
||||||
|
CategoryConsole BreadcrumbCategory = "console"
|
||||||
|
CategoryAuth BreadcrumbCategory = "auth"
|
||||||
|
CategoryBusiness BreadcrumbCategory = "business"
|
||||||
|
CategoryNotification BreadcrumbCategory = "notification"
|
||||||
|
CategoryCustom BreadcrumbCategory = "custom"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Breadcrumb represents an event leading up to an error
|
||||||
|
type Breadcrumb struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Category BreadcrumbCategory `json:"category"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Level SeverityLevel `json:"level,omitempty"`
|
||||||
|
Data map[string]any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// User represents user information for context
|
||||||
|
type User struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Data map[string]any `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackFrame represents a single frame in a stack trace
|
||||||
|
type StackFrame struct {
|
||||||
|
Function string `json:"function,omitempty"`
|
||||||
|
Filename string `json:"filename,omitempty"`
|
||||||
|
Lineno int `json:"lineno,omitempty"`
|
||||||
|
Colno int `json:"colno,omitempty"`
|
||||||
|
Context []string `json:"context,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExceptionInfo represents exception/error information
|
||||||
|
type ExceptionInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Stacktrace []StackFrame `json:"stacktrace,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlatformInfo represents platform/runtime information
|
||||||
|
type PlatformInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
OS string `json:"os,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JourneyContext represents journey context for tracking user flows
|
||||||
|
type JourneyContext struct {
|
||||||
|
JourneyID string `json:"journeyId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CurrentStep string `json:"currentStep,omitempty"`
|
||||||
|
StartedAt time.Time `json:"startedAt"`
|
||||||
|
Metadata map[string]any `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TelemetryEvent represents an event payload sent to the server
|
||||||
|
type TelemetryEvent struct {
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Level SeverityLevel `json:"level"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Exception *ExceptionInfo `json:"exception,omitempty"`
|
||||||
|
User *User `json:"user,omitempty"`
|
||||||
|
Tags map[string]string `json:"tags"`
|
||||||
|
Extra map[string]any `json:"extra"`
|
||||||
|
Breadcrumbs []Breadcrumb `json:"breadcrumbs"`
|
||||||
|
Journey *JourneyContext `json:"journey,omitempty"`
|
||||||
|
Environment string `json:"environment,omitempty"`
|
||||||
|
AppVersion string `json:"appVersion,omitempty"`
|
||||||
|
Platform PlatformInfo `json:"platform"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsedDSN represents parsed DSN components
|
||||||
|
type ParsedDSN struct {
|
||||||
|
PublicKey string
|
||||||
|
Host string
|
||||||
|
Protocol string
|
||||||
|
APIBaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendResult represents the result of sending an event
|
||||||
|
type SendResult struct {
|
||||||
|
Success bool
|
||||||
|
EventID string
|
||||||
|
Error string
|
||||||
|
Queued bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options represents options for initializing the SDK
|
||||||
|
type Options struct {
|
||||||
|
// DSN containing the public key
|
||||||
|
// Format: https://pk_live_xxx@irontelemetry.com
|
||||||
|
DSN string
|
||||||
|
|
||||||
|
// Environment name (e.g., 'production', 'staging')
|
||||||
|
Environment string
|
||||||
|
|
||||||
|
// Application version
|
||||||
|
AppVersion string
|
||||||
|
|
||||||
|
// Sample rate for events (0.0 to 1.0)
|
||||||
|
SampleRate float64
|
||||||
|
|
||||||
|
// Maximum number of breadcrumbs to keep
|
||||||
|
MaxBreadcrumbs int
|
||||||
|
|
||||||
|
// Enable debug logging
|
||||||
|
Debug bool
|
||||||
|
|
||||||
|
// Hook called before sending an event
|
||||||
|
// Return nil to drop the event
|
||||||
|
BeforeSend func(*TelemetryEvent) *TelemetryEvent
|
||||||
|
|
||||||
|
// Enable offline queue for failed events
|
||||||
|
EnableOfflineQueue bool
|
||||||
|
|
||||||
|
// Maximum size of the offline queue
|
||||||
|
MaxOfflineQueueSize int
|
||||||
|
|
||||||
|
// API base URL (defaults to parsed from DSN)
|
||||||
|
APIBaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns Options with default values
|
||||||
|
func DefaultOptions() Options {
|
||||||
|
return Options{
|
||||||
|
Environment: "production",
|
||||||
|
AppVersion: "0.0.0",
|
||||||
|
SampleRate: 1.0,
|
||||||
|
MaxBreadcrumbs: 100,
|
||||||
|
Debug: false,
|
||||||
|
EnableOfflineQueue: true,
|
||||||
|
MaxOfflineQueueSize: 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue