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 }