diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..db207d3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,48 @@
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# Gradle
+.gradle/
+build/
+!gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+# IDE
+.idea/
+*.iml
+*.ipr
+*.iws
+.project
+.classpath
+.settings/
+.vscode/
+*.swp
+*.swo
+out/
+bin/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+logs/
+
+# Package files
+*.jar
+*.war
+*.ear
+*.zip
+*.tar.gz
+*.rar
diff --git a/README.md b/README.md
index 337e4f7..44b708c 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,277 @@
-# irontelemetry-java
-IronTelemetry SDK for Java - Error monitoring and crash reporting
+# IronTelemetry SDK for Java
+
+Error monitoring and crash reporting SDK for Java applications. Capture exceptions, track user journeys, and get insights to fix issues faster.
+
+[](https://search.maven.org/artifact/com.ironservices/telemetry)
+[](https://opensource.org/licenses/MIT)
+
+## Installation
+
+### Maven
+
+```xml
+
+ com.ironservices
+ telemetry
+ 0.1.0
+
+```
+
+### Gradle
+
+```groovy
+implementation 'com.ironservices:telemetry:0.1.0'
+```
+
+## Quick Start
+
+### Basic Exception Capture
+
+```java
+import com.ironservices.telemetry.TelemetryClient;
+
+public class Main {
+ public static void main(String[] args) {
+ // Initialize with your DSN
+ try (TelemetryClient client = new TelemetryClient("https://pk_live_xxx@irontelemetry.com")) {
+ try {
+ doSomething();
+ } catch (Exception e) {
+ client.captureException(e);
+ throw e;
+ }
+ }
+ }
+}
+```
+
+### Journey Tracking
+
+Track user journeys to understand the context of errors:
+
+```java
+import com.ironservices.telemetry.*;
+
+public class CheckoutService {
+ private final TelemetryClient client;
+
+ public CheckoutService(TelemetryClient client) {
+ this.client = client;
+ }
+
+ public void processCheckout() {
+ Journey journey = client.startJourney("Checkout Flow");
+ journey.setUser("user-123", "user@example.com", "John Doe");
+
+ try {
+ journey.runStep("Validate Cart", BreadcrumbCategory.BUSINESS, () -> {
+ validateCart();
+ });
+
+ journey.runStep("Process Payment", BreadcrumbCategory.BUSINESS, () -> {
+ processPayment();
+ });
+
+ journey.runStep("Send Confirmation", BreadcrumbCategory.NOTIFICATION, () -> {
+ sendConfirmationEmail();
+ });
+
+ journey.complete();
+ } catch (Exception e) {
+ journey.fail(e);
+ client.captureException(e);
+ throw e;
+ }
+ }
+}
+```
+
+## Configuration
+
+```java
+import com.ironservices.telemetry.TelemetryClient;
+import com.ironservices.telemetry.TelemetryOptions;
+
+TelemetryOptions options = new TelemetryOptions("https://pk_live_xxx@irontelemetry.com")
+ .setEnvironment("production")
+ .setAppVersion("1.2.3")
+ .setSampleRate(1.0) // 100% of events
+ .setDebug(false)
+ .setBeforeSend(event -> {
+ // Filter or modify events
+ if (event.getMessage() != null && event.getMessage().contains("expected")) {
+ return null; // Drop the event
+ }
+ return event;
+ });
+
+TelemetryClient client = new TelemetryClient(options);
+```
+
+### 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` | double | 1.0 | Sample rate (0.0 to 1.0) |
+| `maxBreadcrumbs` | int | 100 | Max breadcrumbs to keep |
+| `debug` | boolean | false | Enable debug logging |
+| `beforeSend` | Function | null | Hook to filter/modify events |
+| `enableOfflineQueue` | boolean | 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
+- **Async Support**: CompletableFuture support for async operations
+- **Thread-Safe**: All operations are safe for concurrent use
+
+## Breadcrumbs
+
+```java
+// Add simple breadcrumbs
+client.addBreadcrumb("User clicked checkout button", BreadcrumbCategory.UI);
+client.addBreadcrumb("Payment API called", BreadcrumbCategory.HTTP);
+
+// With level
+client.addBreadcrumb("User logged in", BreadcrumbCategory.AUTH, SeverityLevel.INFO);
+
+// With data
+Map data = new HashMap<>();
+data.put("url", "/api/checkout");
+data.put("statusCode", 200);
+data.put("duration", 150);
+client.addBreadcrumb("API request completed", BreadcrumbCategory.HTTP, SeverityLevel.INFO, data);
+
+// Using builder
+client.addBreadcrumb(Breadcrumb.builder("Payment processed", BreadcrumbCategory.BUSINESS)
+ .level(SeverityLevel.INFO)
+ .addData("amount", 99.99)
+ .addData("currency", "USD")
+ .build());
+```
+
+### Breadcrumb Categories
+
+```java
+BreadcrumbCategory.UI // User interface interactions
+BreadcrumbCategory.HTTP // HTTP requests
+BreadcrumbCategory.NAVIGATION // Page/route navigation
+BreadcrumbCategory.CONSOLE // Console output
+BreadcrumbCategory.AUTH // Authentication events
+BreadcrumbCategory.BUSINESS // Business logic events
+BreadcrumbCategory.NOTIFICATION // Notification events
+BreadcrumbCategory.CUSTOM // Custom events
+```
+
+## Severity Levels
+
+```java
+SeverityLevel.DEBUG
+SeverityLevel.INFO
+SeverityLevel.WARNING
+SeverityLevel.ERROR
+SeverityLevel.FATAL
+```
+
+## User Context
+
+```java
+// Simple user ID
+client.setUser("user-123");
+
+// With email
+client.setUser("user-123", "user@example.com");
+
+// Full user object
+client.setUser(User.builder("user-123")
+ .email("user@example.com")
+ .name("John Doe")
+ .addData("plan", "premium")
+ .build());
+```
+
+## Tags and Extra Data
+
+```java
+// Set individual tags
+client.setTag("release", "v1.2.3");
+client.setTag("server", "prod-1");
+
+// Set multiple tags
+Map tags = new HashMap<>();
+tags.put("release", "v1.2.3");
+tags.put("server", "prod-1");
+client.setTags(tags);
+
+// Set extra data
+client.setExtra("request_id", "abc-123");
+
+Map extras = new HashMap<>();
+extras.put("request_id", "abc-123");
+extras.put("user_agent", "Mozilla/5.0...");
+client.setExtras(extras);
+```
+
+## Async Operations
+
+```java
+// Async exception capture
+CompletableFuture future = client.captureExceptionAsync(exception);
+future.thenAccept(result -> {
+ if (result.isSuccess()) {
+ System.out.println("Event sent: " + result.getEventId());
+ }
+});
+
+// Async message capture
+client.captureMessageAsync("Something happened", SeverityLevel.WARNING)
+ .thenAccept(result -> System.out.println("Sent: " + result.isSuccess()));
+```
+
+## Spring Integration Example
+
+```java
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+@ControllerAdvice
+public class GlobalExceptionHandler {
+ private final TelemetryClient telemetryClient;
+
+ public GlobalExceptionHandler(TelemetryClient telemetryClient) {
+ this.telemetryClient = telemetryClient;
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity> handleException(Exception e, HttpServletRequest request) {
+ telemetryClient.addBreadcrumb(
+ "HTTP Request: " + request.getMethod() + " " + request.getRequestURI(),
+ BreadcrumbCategory.HTTP
+ );
+ telemetryClient.captureException(e);
+ return ResponseEntity.status(500).body("Internal Server Error");
+ }
+}
+```
+
+## Requirements
+
+- Java 11+
+- OkHttp 4.x
+- Gson 2.x
+
+## Links
+
+- [Documentation](https://www.irontelemetry.com/docs)
+- [Dashboard](https://www.irontelemetry.com)
+
+## License
+
+MIT License - see [LICENSE](LICENSE) for details.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6b6dcea
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,106 @@
+
+
+ 4.0.0
+
+ com.ironservices
+ telemetry
+ 0.1.0
+ jar
+
+ IronTelemetry
+ Error monitoring and crash reporting SDK for Java applications
+ https://github.com/IronServices/irontelemetry-java
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ IronServices
+ support@ironservices.com
+ IronServices
+ https://www.ironservices.com
+
+
+
+
+ scm:git:git://github.com/IronServices/irontelemetry-java.git
+ scm:git:ssh://github.com:IronServices/irontelemetry-java.git
+ https://github.com/IronServices/irontelemetry-java
+
+
+
+ 11
+ 11
+ UTF-8
+ 2.10.1
+ 4.12.0
+
+
+
+
+ com.google.code.gson
+ gson
+ ${gson.version}
+
+
+ com.squareup.okhttp3
+ okhttp
+ ${okhttp.version}
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.1
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 11
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/src/main/java/com/ironservices/telemetry/Breadcrumb.java b/src/main/java/com/ironservices/telemetry/Breadcrumb.java
new file mode 100644
index 0000000..b76b8a6
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/Breadcrumb.java
@@ -0,0 +1,85 @@
+package com.ironservices.telemetry;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a breadcrumb event leading up to an error.
+ */
+public class Breadcrumb {
+ private final Instant timestamp;
+ private final BreadcrumbCategory category;
+ private final String message;
+ private final SeverityLevel level;
+ private final Map data;
+
+ private Breadcrumb(Builder builder) {
+ this.timestamp = builder.timestamp != null ? builder.timestamp : Instant.now();
+ this.category = builder.category;
+ this.message = builder.message;
+ this.level = builder.level != null ? builder.level : SeverityLevel.INFO;
+ this.data = builder.data;
+ }
+
+ public Instant getTimestamp() {
+ return timestamp;
+ }
+
+ public BreadcrumbCategory getCategory() {
+ return category;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public SeverityLevel getLevel() {
+ return level;
+ }
+
+ public Map getData() {
+ return data;
+ }
+
+ public static Builder builder(String message, BreadcrumbCategory category) {
+ return new Builder(message, category);
+ }
+
+ public static class Builder {
+ private Instant timestamp;
+ private final BreadcrumbCategory category;
+ private final String message;
+ private SeverityLevel level;
+ private Map data = new HashMap<>();
+
+ public Builder(String message, BreadcrumbCategory category) {
+ this.message = message;
+ this.category = category;
+ }
+
+ public Builder timestamp(Instant timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public Builder level(SeverityLevel level) {
+ this.level = level;
+ return this;
+ }
+
+ public Builder data(Map data) {
+ this.data = data;
+ return this;
+ }
+
+ public Builder addData(String key, Object value) {
+ this.data.put(key, value);
+ return this;
+ }
+
+ public Breadcrumb build() {
+ return new Breadcrumb(this);
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/BreadcrumbCategory.java b/src/main/java/com/ironservices/telemetry/BreadcrumbCategory.java
new file mode 100644
index 0000000..24ff8d8
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/BreadcrumbCategory.java
@@ -0,0 +1,30 @@
+package com.ironservices.telemetry;
+
+/**
+ * Category for breadcrumbs.
+ */
+public enum BreadcrumbCategory {
+ UI("ui"),
+ HTTP("http"),
+ NAVIGATION("navigation"),
+ CONSOLE("console"),
+ AUTH("auth"),
+ BUSINESS("business"),
+ NOTIFICATION("notification"),
+ CUSTOM("custom");
+
+ private final String value;
+
+ BreadcrumbCategory(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/BreadcrumbManager.java b/src/main/java/com/ironservices/telemetry/BreadcrumbManager.java
new file mode 100644
index 0000000..fd75864
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/BreadcrumbManager.java
@@ -0,0 +1,80 @@
+package com.ironservices.telemetry;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages a ring buffer of breadcrumbs.
+ */
+public class BreadcrumbManager {
+ private final LinkedList breadcrumbs;
+ private final int maxBreadcrumbs;
+ private final Object lock = new Object();
+
+ public BreadcrumbManager(int maxBreadcrumbs) {
+ this.breadcrumbs = new LinkedList<>();
+ this.maxBreadcrumbs = maxBreadcrumbs;
+ }
+
+ /**
+ * Add a breadcrumb to the ring buffer.
+ */
+ public void add(Breadcrumb breadcrumb) {
+ synchronized (lock) {
+ if (breadcrumbs.size() >= maxBreadcrumbs) {
+ breadcrumbs.removeFirst();
+ }
+ breadcrumbs.add(breadcrumb);
+ }
+ }
+
+ /**
+ * Add a simple breadcrumb with just a message and category.
+ */
+ public void add(String message, BreadcrumbCategory category) {
+ add(Breadcrumb.builder(message, category).build());
+ }
+
+ /**
+ * Add a breadcrumb with a specific level.
+ */
+ public void add(String message, BreadcrumbCategory category, SeverityLevel level) {
+ add(Breadcrumb.builder(message, category).level(level).build());
+ }
+
+ /**
+ * Add a breadcrumb with additional data.
+ */
+ public void add(String message, BreadcrumbCategory category, SeverityLevel level, Map data) {
+ add(Breadcrumb.builder(message, category).level(level).data(data).build());
+ }
+
+ /**
+ * Get all breadcrumbs as a copy.
+ */
+ public List getAll() {
+ synchronized (lock) {
+ return new ArrayList<>(breadcrumbs);
+ }
+ }
+
+ /**
+ * Clear all breadcrumbs.
+ */
+ public void clear() {
+ synchronized (lock) {
+ breadcrumbs.clear();
+ }
+ }
+
+ /**
+ * Get the number of breadcrumbs.
+ */
+ public int size() {
+ synchronized (lock) {
+ return breadcrumbs.size();
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/ExceptionInfo.java b/src/main/java/com/ironservices/telemetry/ExceptionInfo.java
new file mode 100644
index 0000000..19d91bc
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/ExceptionInfo.java
@@ -0,0 +1,43 @@
+package com.ironservices.telemetry;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents exception/error information.
+ */
+public class ExceptionInfo {
+ private final String type;
+ private final String message;
+ private final List stacktrace;
+
+ public ExceptionInfo(String type, String message, List stacktrace) {
+ this.type = type;
+ this.message = message;
+ this.stacktrace = stacktrace;
+ }
+
+ public static ExceptionInfo fromThrowable(Throwable throwable) {
+ List frames = new ArrayList<>();
+ for (StackTraceElement element : throwable.getStackTrace()) {
+ frames.add(new StackFrame(element));
+ }
+ return new ExceptionInfo(
+ throwable.getClass().getName(),
+ throwable.getMessage(),
+ frames
+ );
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public List getStacktrace() {
+ return stacktrace;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/Journey.java b/src/main/java/com/ironservices/telemetry/Journey.java
new file mode 100644
index 0000000..843707a
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/Journey.java
@@ -0,0 +1,123 @@
+package com.ironservices.telemetry;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+/**
+ * Represents an active journey.
+ */
+public class Journey {
+ private final JourneyManager manager;
+ private final JourneyContext context;
+
+ Journey(JourneyManager manager, JourneyContext context) {
+ this.manager = manager;
+ this.context = context;
+ }
+
+ /**
+ * Set the user for the journey.
+ */
+ public Journey setUser(String id, String email, String name) {
+ context.setMetadata("userId", id);
+ if (email != null) {
+ context.setMetadata("userEmail", email);
+ }
+ if (name != null) {
+ context.setMetadata("userName", name);
+ }
+ return this;
+ }
+
+ /**
+ * Set metadata for the journey.
+ */
+ public Journey setMetadata(String key, Object value) {
+ context.setMetadata(key, value);
+ return this;
+ }
+
+ /**
+ * Start a new step in the journey.
+ */
+ public Step startStep(String name, BreadcrumbCategory category) {
+ context.setCurrentStep(name);
+
+ TelemetryClient client = manager.getClient();
+ if (client != null) {
+ Map data = new HashMap<>();
+ data.put("journeyId", context.getJourneyId());
+ data.put("journeyName", context.getName());
+ client.addBreadcrumb("Started step: " + name, category, SeverityLevel.INFO, data);
+ }
+
+ return new Step(this, name, category);
+ }
+
+ /**
+ * Run a function as a step.
+ */
+ public T runStep(String name, BreadcrumbCategory category, Callable fn) throws Exception {
+ Step step = startStep(name, category);
+ try {
+ T result = fn.call();
+ step.complete();
+ return result;
+ } catch (Exception e) {
+ step.fail(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Run a void function as a step.
+ */
+ public void runStep(String name, BreadcrumbCategory category, Runnable fn) {
+ Step step = startStep(name, category);
+ try {
+ fn.run();
+ step.complete();
+ } catch (Exception e) {
+ step.fail(e);
+ throw e;
+ }
+ }
+
+ /**
+ * Complete the journey successfully.
+ */
+ public void complete() {
+ TelemetryClient client = manager.getClient();
+ if (client != null) {
+ Map data = new HashMap<>();
+ data.put("journeyId", context.getJourneyId());
+ data.put("duration", Duration.between(context.getStartedAt(), Instant.now()).toMillis());
+ client.addBreadcrumb("Completed journey: " + context.getName(), BreadcrumbCategory.BUSINESS, SeverityLevel.INFO, data);
+ }
+ manager.clear();
+ }
+
+ /**
+ * Mark the journey as failed.
+ */
+ public void fail(Throwable error) {
+ TelemetryClient client = manager.getClient();
+ if (client != null) {
+ Map data = new HashMap<>();
+ data.put("journeyId", context.getJourneyId());
+ data.put("duration", Duration.between(context.getStartedAt(), Instant.now()).toMillis());
+ if (error != null) {
+ data.put("error", error.getMessage());
+ }
+ client.addBreadcrumb("Failed journey: " + context.getName(), BreadcrumbCategory.BUSINESS, SeverityLevel.ERROR, data);
+ }
+ manager.clear();
+ }
+
+ JourneyContext getContext() {
+ return context;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/JourneyContext.java b/src/main/java/com/ironservices/telemetry/JourneyContext.java
new file mode 100644
index 0000000..6b4381d
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/JourneyContext.java
@@ -0,0 +1,51 @@
+package com.ironservices.telemetry;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents journey context for tracking user flows.
+ */
+public class JourneyContext {
+ private final String journeyId;
+ private final String name;
+ private String currentStep;
+ private final Instant startedAt;
+ private final Map metadata;
+
+ public JourneyContext(String journeyId, String name) {
+ this.journeyId = journeyId;
+ this.name = name;
+ this.startedAt = Instant.now();
+ this.metadata = new HashMap<>();
+ }
+
+ public String getJourneyId() {
+ return journeyId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getCurrentStep() {
+ return currentStep;
+ }
+
+ public void setCurrentStep(String currentStep) {
+ this.currentStep = currentStep;
+ }
+
+ public Instant getStartedAt() {
+ return startedAt;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(String key, Object value) {
+ this.metadata.put(key, value);
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/JourneyManager.java b/src/main/java/com/ironservices/telemetry/JourneyManager.java
new file mode 100644
index 0000000..aa2b8fe
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/JourneyManager.java
@@ -0,0 +1,64 @@
+package com.ironservices.telemetry;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.WeakHashMap;
+
+/**
+ * Manages journey context.
+ */
+public class JourneyManager {
+ private static final Map journeyClients = new WeakHashMap<>();
+
+ private final TelemetryClient client;
+ private volatile JourneyContext current;
+ private final Object lock = new Object();
+
+ public JourneyManager(TelemetryClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Start a new journey.
+ */
+ public Journey startJourney(String name) {
+ synchronized (lock) {
+ JourneyContext context = new JourneyContext(UUID.randomUUID().toString(), name);
+ this.current = context;
+
+ if (client != null) {
+ client.addBreadcrumb("Started journey: " + name, BreadcrumbCategory.BUSINESS);
+ }
+
+ Journey journey = new Journey(this, context);
+ journeyClients.put(journey, client);
+ return journey;
+ }
+ }
+
+ /**
+ * Get the current journey context.
+ */
+ public JourneyContext getCurrent() {
+ synchronized (lock) {
+ return current;
+ }
+ }
+
+ /**
+ * Clear the current journey.
+ */
+ public void clear() {
+ synchronized (lock) {
+ current = null;
+ }
+ }
+
+ TelemetryClient getClient() {
+ return client;
+ }
+
+ static TelemetryClient getClientForJourney(Journey journey) {
+ return journeyClients.get(journey);
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/ParsedDSN.java b/src/main/java/com/ironservices/telemetry/ParsedDSN.java
new file mode 100644
index 0000000..ed40473
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/ParsedDSN.java
@@ -0,0 +1,69 @@
+package com.ironservices.telemetry;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Represents parsed DSN components.
+ */
+public class ParsedDSN {
+ private final String publicKey;
+ private final String host;
+ private final String protocol;
+ private final String apiBaseUrl;
+
+ private ParsedDSN(String publicKey, String host, String protocol, String apiBaseUrl) {
+ this.publicKey = publicKey;
+ this.host = host;
+ this.protocol = protocol;
+ this.apiBaseUrl = apiBaseUrl;
+ }
+
+ /**
+ * Parse a DSN string.
+ * Format: https://pk_live_xxx@irontelemetry.com
+ */
+ public static ParsedDSN parse(String dsn) {
+ if (dsn == null || dsn.isEmpty()) {
+ throw new IllegalArgumentException("DSN cannot be null or empty");
+ }
+
+ try {
+ URI uri = new URI(dsn);
+ String userInfo = uri.getUserInfo();
+
+ if (userInfo == null || !userInfo.startsWith("pk_")) {
+ throw new IllegalArgumentException("DSN must contain a valid public key starting with pk_");
+ }
+
+ String protocol = uri.getScheme();
+ String host = uri.getHost();
+ int port = uri.getPort();
+
+ String apiBaseUrl = protocol + "://" + host;
+ if (port > 0 && port != 80 && port != 443) {
+ apiBaseUrl += ":" + port;
+ }
+
+ return new ParsedDSN(userInfo, host, protocol, apiBaseUrl);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid DSN format: " + dsn, e);
+ }
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public String getApiBaseUrl() {
+ return apiBaseUrl;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/PlatformInfo.java b/src/main/java/com/ironservices/telemetry/PlatformInfo.java
new file mode 100644
index 0000000..8cc9062
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/PlatformInfo.java
@@ -0,0 +1,36 @@
+package com.ironservices.telemetry;
+
+/**
+ * Represents platform/runtime information.
+ */
+public class PlatformInfo {
+ private final String name;
+ private final String version;
+ private final String os;
+
+ public PlatformInfo(String name, String version, String os) {
+ this.name = name;
+ this.version = version;
+ this.os = os;
+ }
+
+ public static PlatformInfo current() {
+ return new PlatformInfo(
+ "java",
+ System.getProperty("java.version"),
+ System.getProperty("os.name") + " " + System.getProperty("os.version")
+ );
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public String getOs() {
+ return os;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/SendResult.java b/src/main/java/com/ironservices/telemetry/SendResult.java
new file mode 100644
index 0000000..410a1c6
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/SendResult.java
@@ -0,0 +1,46 @@
+package com.ironservices.telemetry;
+
+/**
+ * Represents the result of sending an event.
+ */
+public class SendResult {
+ private final boolean success;
+ private final String eventId;
+ private final String error;
+ private final boolean queued;
+
+ private SendResult(boolean success, String eventId, String error, boolean queued) {
+ this.success = success;
+ this.eventId = eventId;
+ this.error = error;
+ this.queued = queued;
+ }
+
+ public static SendResult success(String eventId) {
+ return new SendResult(true, eventId, null, false);
+ }
+
+ public static SendResult failure(String error) {
+ return new SendResult(false, null, error, false);
+ }
+
+ public static SendResult queued(String eventId) {
+ return new SendResult(true, eventId, null, true);
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getEventId() {
+ return eventId;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public boolean isQueued() {
+ return queued;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/SeverityLevel.java b/src/main/java/com/ironservices/telemetry/SeverityLevel.java
new file mode 100644
index 0000000..fb046b3
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/SeverityLevel.java
@@ -0,0 +1,27 @@
+package com.ironservices.telemetry;
+
+/**
+ * Severity level for telemetry events.
+ */
+public enum SeverityLevel {
+ DEBUG("debug"),
+ INFO("info"),
+ WARNING("warning"),
+ ERROR("error"),
+ FATAL("fatal");
+
+ private final String value;
+
+ SeverityLevel(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/StackFrame.java b/src/main/java/com/ironservices/telemetry/StackFrame.java
new file mode 100644
index 0000000..18185cf
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/StackFrame.java
@@ -0,0 +1,41 @@
+package com.ironservices.telemetry;
+
+/**
+ * Represents a single frame in a stack trace.
+ */
+public class StackFrame {
+ private final String function;
+ private final String filename;
+ private final int lineno;
+ private final int colno;
+
+ public StackFrame(String function, String filename, int lineno, int colno) {
+ this.function = function;
+ this.filename = filename;
+ this.lineno = lineno;
+ this.colno = colno;
+ }
+
+ public StackFrame(StackTraceElement element) {
+ this.function = element.getClassName() + "." + element.getMethodName();
+ this.filename = element.getFileName();
+ this.lineno = element.getLineNumber();
+ this.colno = 0;
+ }
+
+ public String getFunction() {
+ return function;
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public int getLineno() {
+ return lineno;
+ }
+
+ public int getColno() {
+ return colno;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/Step.java b/src/main/java/com/ironservices/telemetry/Step.java
new file mode 100644
index 0000000..e9723ea
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/Step.java
@@ -0,0 +1,69 @@
+package com.ironservices.telemetry;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a step within a journey.
+ */
+public class Step {
+ private final Journey journey;
+ private final String name;
+ private final BreadcrumbCategory category;
+ private final Instant startedAt;
+ private final Map data;
+
+ Step(Journey journey, String name, BreadcrumbCategory category) {
+ this.journey = journey;
+ this.name = name;
+ this.category = category;
+ this.startedAt = Instant.now();
+ this.data = new HashMap<>();
+ }
+
+ /**
+ * Set data for the step.
+ */
+ public Step setData(String key, Object value) {
+ data.put(key, value);
+ return this;
+ }
+
+ /**
+ * Complete the step successfully.
+ */
+ public void complete() {
+ TelemetryClient client = journey.getContext() != null ?
+ JourneyManager.getClientForJourney(journey) : null;
+
+ if (client != null) {
+ Map breadcrumbData = new HashMap<>();
+ breadcrumbData.put("journeyId", journey.getContext().getJourneyId());
+ breadcrumbData.put("journeyName", journey.getContext().getName());
+ breadcrumbData.put("duration", Duration.between(startedAt, Instant.now()).toMillis());
+ breadcrumbData.putAll(data);
+ client.addBreadcrumb("Completed step: " + name, category, SeverityLevel.INFO, breadcrumbData);
+ }
+ }
+
+ /**
+ * Mark the step as failed.
+ */
+ public void fail(Throwable error) {
+ TelemetryClient client = JourneyManager.getClientForJourney(journey);
+
+ if (client != null) {
+ Map breadcrumbData = new HashMap<>();
+ breadcrumbData.put("journeyId", journey.getContext().getJourneyId());
+ breadcrumbData.put("journeyName", journey.getContext().getName());
+ breadcrumbData.put("duration", Duration.between(startedAt, Instant.now()).toMillis());
+ breadcrumbData.putAll(data);
+ if (error != null) {
+ breadcrumbData.put("error", error.getMessage());
+ }
+ client.addBreadcrumb("Failed step: " + name, category, SeverityLevel.ERROR, breadcrumbData);
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/TelemetryClient.java b/src/main/java/com/ironservices/telemetry/TelemetryClient.java
new file mode 100644
index 0000000..ce426bb
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/TelemetryClient.java
@@ -0,0 +1,262 @@
+package com.ironservices.telemetry;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * The main IronTelemetry client.
+ */
+public class TelemetryClient implements AutoCloseable {
+ private final TelemetryOptions options;
+ private final ParsedDSN parsedDSN;
+ private final Transport transport;
+ private final BreadcrumbManager breadcrumbs;
+ private final JourneyManager journeys;
+ private volatile User user;
+ private final Map tags;
+ private final Map extra;
+ private final Object lock = new Object();
+
+ /**
+ * Create a new TelemetryClient with a DSN.
+ */
+ public TelemetryClient(String dsn) {
+ this(new TelemetryOptions(dsn));
+ }
+
+ /**
+ * Create a new TelemetryClient with options.
+ */
+ public TelemetryClient(TelemetryOptions options) {
+ this.options = options;
+ this.parsedDSN = ParsedDSN.parse(options.getDsn());
+ String apiBaseUrl = options.getApiBaseUrl() != null ? options.getApiBaseUrl() : parsedDSN.getApiBaseUrl();
+ this.transport = new Transport(parsedDSN, apiBaseUrl, options.isDebug());
+ this.breadcrumbs = new BreadcrumbManager(options.getMaxBreadcrumbs());
+ this.journeys = new JourneyManager(this);
+ this.tags = new HashMap<>();
+ this.extra = new HashMap<>();
+
+ if (options.isDebug()) {
+ System.out.println("[IronTelemetry] Initialized with DSN: " + parsedDSN.getApiBaseUrl());
+ }
+ }
+
+ /**
+ * Capture an exception and send it to the server.
+ */
+ public SendResult captureException(Throwable throwable) {
+ if (throwable == null) {
+ return SendResult.failure("null exception");
+ }
+
+ TelemetryEvent event = createEvent(SeverityLevel.ERROR, throwable.getMessage());
+ event.setException(ExceptionInfo.fromThrowable(throwable));
+ return sendEvent(event);
+ }
+
+ /**
+ * Capture an exception asynchronously.
+ */
+ public CompletableFuture captureExceptionAsync(Throwable throwable) {
+ return CompletableFuture.supplyAsync(() -> captureException(throwable));
+ }
+
+ /**
+ * Capture a message and send it to the server.
+ */
+ public SendResult captureMessage(String message, SeverityLevel level) {
+ TelemetryEvent event = createEvent(level, message);
+ return sendEvent(event);
+ }
+
+ /**
+ * Capture a message with INFO level.
+ */
+ public SendResult captureMessage(String message) {
+ return captureMessage(message, SeverityLevel.INFO);
+ }
+
+ /**
+ * Capture a message asynchronously.
+ */
+ public CompletableFuture captureMessageAsync(String message, SeverityLevel level) {
+ return CompletableFuture.supplyAsync(() -> captureMessage(message, level));
+ }
+
+ /**
+ * Add a breadcrumb.
+ */
+ public void addBreadcrumb(String message, BreadcrumbCategory category) {
+ breadcrumbs.add(message, category);
+ }
+
+ /**
+ * Add a breadcrumb with a level.
+ */
+ public void addBreadcrumb(String message, BreadcrumbCategory category, SeverityLevel level) {
+ breadcrumbs.add(message, category, level);
+ }
+
+ /**
+ * Add a breadcrumb with data.
+ */
+ public void addBreadcrumb(String message, BreadcrumbCategory category, SeverityLevel level, Map data) {
+ breadcrumbs.add(message, category, level, data);
+ }
+
+ /**
+ * Add a breadcrumb object.
+ */
+ public void addBreadcrumb(Breadcrumb breadcrumb) {
+ breadcrumbs.add(breadcrumb);
+ }
+
+ /**
+ * Set the user context.
+ */
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ /**
+ * Set the user context by ID.
+ */
+ public void setUser(String id) {
+ this.user = User.builder(id).build();
+ }
+
+ /**
+ * Set the user context with ID and email.
+ */
+ public void setUser(String id, String email) {
+ this.user = User.builder(id).email(email).build();
+ }
+
+ /**
+ * Set a tag.
+ */
+ public void setTag(String key, String value) {
+ synchronized (lock) {
+ tags.put(key, value);
+ }
+ }
+
+ /**
+ * Set multiple tags.
+ */
+ public void setTags(Map tags) {
+ synchronized (lock) {
+ this.tags.putAll(tags);
+ }
+ }
+
+ /**
+ * Set extra data.
+ */
+ public void setExtra(String key, Object value) {
+ synchronized (lock) {
+ extra.put(key, value);
+ }
+ }
+
+ /**
+ * Set multiple extra data values.
+ */
+ public void setExtras(Map extras) {
+ synchronized (lock) {
+ this.extra.putAll(extras);
+ }
+ }
+
+ /**
+ * Start a new journey.
+ */
+ public Journey startJourney(String name) {
+ return journeys.startJourney(name);
+ }
+
+ /**
+ * Get the current journey context.
+ */
+ public JourneyContext getCurrentJourney() {
+ return journeys.getCurrent();
+ }
+
+ /**
+ * Clear all breadcrumbs.
+ */
+ public void clearBreadcrumbs() {
+ breadcrumbs.clear();
+ }
+
+ /**
+ * Flush pending events.
+ */
+ public void flush() {
+ // Future: Implement offline queue flushing
+ }
+
+ /**
+ * Close the client and release resources.
+ */
+ @Override
+ public void close() {
+ flush();
+ transport.close();
+ }
+
+ private TelemetryEvent createEvent(SeverityLevel level, String message) {
+ TelemetryEvent event = new TelemetryEvent(
+ UUID.randomUUID().toString(),
+ level,
+ message
+ );
+
+ event.setEnvironment(options.getEnvironment());
+ event.setAppVersion(options.getAppVersion());
+ event.setBreadcrumbs(breadcrumbs.getAll());
+
+ synchronized (lock) {
+ event.setTags(new HashMap<>(tags));
+ event.setExtra(new HashMap<>(extra));
+ }
+
+ if (user != null) {
+ event.setUser(user);
+ }
+
+ JourneyContext journey = journeys.getCurrent();
+ if (journey != null) {
+ event.setJourney(journey);
+ }
+
+ return event;
+ }
+
+ private SendResult sendEvent(TelemetryEvent event) {
+ // Check sample rate
+ if (options.getSampleRate() < 1.0 && ThreadLocalRandom.current().nextDouble() > options.getSampleRate()) {
+ if (options.isDebug()) {
+ System.out.println("[IronTelemetry] Event sampled out: " + event.getEventId());
+ }
+ return SendResult.success(event.getEventId());
+ }
+
+ // Apply beforeSend hook
+ if (options.getBeforeSend() != null) {
+ event = options.getBeforeSend().apply(event);
+ if (event == null) {
+ if (options.isDebug()) {
+ System.out.println("[IronTelemetry] Event dropped by beforeSend hook");
+ }
+ return SendResult.success(null);
+ }
+ }
+
+ return transport.send(event);
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/TelemetryEvent.java b/src/main/java/com/ironservices/telemetry/TelemetryEvent.java
new file mode 100644
index 0000000..385a912
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/TelemetryEvent.java
@@ -0,0 +1,125 @@
+package com.ironservices.telemetry;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a telemetry event payload sent to the server.
+ */
+public class TelemetryEvent {
+ private final String eventId;
+ private final Instant timestamp;
+ private final SeverityLevel level;
+ private final String message;
+ private ExceptionInfo exception;
+ private User user;
+ private Map tags;
+ private Map extra;
+ private List breadcrumbs;
+ private JourneyContext journey;
+ private String environment;
+ private String appVersion;
+ private PlatformInfo platform;
+
+ public TelemetryEvent(String eventId, SeverityLevel level, String message) {
+ this.eventId = eventId;
+ this.timestamp = Instant.now();
+ this.level = level;
+ this.message = message;
+ this.tags = new HashMap<>();
+ this.extra = new HashMap<>();
+ this.breadcrumbs = new ArrayList<>();
+ this.platform = PlatformInfo.current();
+ }
+
+ public String getEventId() {
+ return eventId;
+ }
+
+ public Instant getTimestamp() {
+ return timestamp;
+ }
+
+ public SeverityLevel getLevel() {
+ return level;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public ExceptionInfo getException() {
+ return exception;
+ }
+
+ public void setException(ExceptionInfo exception) {
+ this.exception = exception;
+ }
+
+ public User getUser() {
+ return user;
+ }
+
+ public void setUser(User user) {
+ this.user = user;
+ }
+
+ public Map getTags() {
+ return tags;
+ }
+
+ public void setTags(Map tags) {
+ this.tags = tags;
+ }
+
+ public Map getExtra() {
+ return extra;
+ }
+
+ public void setExtra(Map extra) {
+ this.extra = extra;
+ }
+
+ public List getBreadcrumbs() {
+ return breadcrumbs;
+ }
+
+ public void setBreadcrumbs(List breadcrumbs) {
+ this.breadcrumbs = breadcrumbs;
+ }
+
+ public JourneyContext getJourney() {
+ return journey;
+ }
+
+ public void setJourney(JourneyContext journey) {
+ this.journey = journey;
+ }
+
+ public String getEnvironment() {
+ return environment;
+ }
+
+ public void setEnvironment(String environment) {
+ this.environment = environment;
+ }
+
+ public String getAppVersion() {
+ return appVersion;
+ }
+
+ public void setAppVersion(String appVersion) {
+ this.appVersion = appVersion;
+ }
+
+ public PlatformInfo getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(PlatformInfo platform) {
+ this.platform = platform;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/TelemetryOptions.java b/src/main/java/com/ironservices/telemetry/TelemetryOptions.java
new file mode 100644
index 0000000..63ce016
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/TelemetryOptions.java
@@ -0,0 +1,111 @@
+package com.ironservices.telemetry;
+
+import java.util.function.Function;
+
+/**
+ * Configuration options for the telemetry client.
+ */
+public class TelemetryOptions {
+ private final String dsn;
+ private String environment = "production";
+ private String appVersion = "0.0.0";
+ private double sampleRate = 1.0;
+ private int maxBreadcrumbs = 100;
+ private boolean debug = false;
+ private Function beforeSend;
+ private boolean enableOfflineQueue = true;
+ private int maxOfflineQueueSize = 500;
+ private String apiBaseUrl;
+
+ public TelemetryOptions(String dsn) {
+ if (dsn == null || dsn.isEmpty()) {
+ throw new IllegalArgumentException("DSN is required");
+ }
+ this.dsn = dsn;
+ }
+
+ public String getDsn() {
+ return dsn;
+ }
+
+ public String getEnvironment() {
+ return environment;
+ }
+
+ public TelemetryOptions setEnvironment(String environment) {
+ this.environment = environment;
+ return this;
+ }
+
+ public String getAppVersion() {
+ return appVersion;
+ }
+
+ public TelemetryOptions setAppVersion(String appVersion) {
+ this.appVersion = appVersion;
+ return this;
+ }
+
+ public double getSampleRate() {
+ return sampleRate;
+ }
+
+ public TelemetryOptions setSampleRate(double sampleRate) {
+ this.sampleRate = Math.max(0.0, Math.min(1.0, sampleRate));
+ return this;
+ }
+
+ public int getMaxBreadcrumbs() {
+ return maxBreadcrumbs;
+ }
+
+ public TelemetryOptions setMaxBreadcrumbs(int maxBreadcrumbs) {
+ this.maxBreadcrumbs = maxBreadcrumbs;
+ return this;
+ }
+
+ public boolean isDebug() {
+ return debug;
+ }
+
+ public TelemetryOptions setDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ public Function getBeforeSend() {
+ return beforeSend;
+ }
+
+ public TelemetryOptions setBeforeSend(Function beforeSend) {
+ this.beforeSend = beforeSend;
+ return this;
+ }
+
+ public boolean isEnableOfflineQueue() {
+ return enableOfflineQueue;
+ }
+
+ public TelemetryOptions setEnableOfflineQueue(boolean enableOfflineQueue) {
+ this.enableOfflineQueue = enableOfflineQueue;
+ return this;
+ }
+
+ public int getMaxOfflineQueueSize() {
+ return maxOfflineQueueSize;
+ }
+
+ public TelemetryOptions setMaxOfflineQueueSize(int maxOfflineQueueSize) {
+ this.maxOfflineQueueSize = maxOfflineQueueSize;
+ return this;
+ }
+
+ public String getApiBaseUrl() {
+ return apiBaseUrl;
+ }
+
+ public TelemetryOptions setApiBaseUrl(String apiBaseUrl) {
+ this.apiBaseUrl = apiBaseUrl;
+ return this;
+ }
+}
diff --git a/src/main/java/com/ironservices/telemetry/Transport.java b/src/main/java/com/ironservices/telemetry/Transport.java
new file mode 100644
index 0000000..663b724
--- /dev/null
+++ b/src/main/java/com/ironservices/telemetry/Transport.java
@@ -0,0 +1,190 @@
+package com.ironservices.telemetry;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Handles HTTP communication with the server.
+ */
+public class Transport {
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+ private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_INSTANT;
+
+ private final String apiBaseUrl;
+ private final String publicKey;
+ private final boolean debug;
+ private final OkHttpClient client;
+ private final Gson gson;
+
+ public Transport(ParsedDSN parsedDSN, String apiBaseUrl, boolean debug) {
+ this.apiBaseUrl = apiBaseUrl != null ? apiBaseUrl : parsedDSN.getApiBaseUrl();
+ this.publicKey = parsedDSN.getPublicKey();
+ this.debug = debug;
+ this.client = new OkHttpClient.Builder()
+ .connectTimeout(30, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .writeTimeout(30, TimeUnit.SECONDS)
+ .build();
+ this.gson = new GsonBuilder().serializeNulls().create();
+ }
+
+ /**
+ * Send an event synchronously.
+ */
+ public SendResult send(TelemetryEvent event) {
+ String url = apiBaseUrl + "/api/v1/events";
+
+ try {
+ String body = gson.toJson(serializeEvent(event));
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Content-Type", "application/json")
+ .header("X-Public-Key", publicKey)
+ .post(RequestBody.create(body, JSON))
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ String errorBody = response.body() != null ? response.body().string() : "";
+ String error = "HTTP " + response.code() + ": " + errorBody;
+ if (debug) {
+ System.out.println("[IronTelemetry] Failed to send event: " + error);
+ }
+ return SendResult.failure(error);
+ }
+
+ if (debug) {
+ System.out.println("[IronTelemetry] Event sent successfully: " + event.getEventId());
+ }
+ return SendResult.success(event.getEventId());
+ }
+ } catch (IOException e) {
+ if (debug) {
+ System.out.println("[IronTelemetry] Failed to send event: " + e.getMessage());
+ }
+ return SendResult.failure(e.getMessage());
+ }
+ }
+
+ /**
+ * Send an event asynchronously.
+ */
+ public CompletableFuture sendAsync(TelemetryEvent event) {
+ return CompletableFuture.supplyAsync(() -> send(event));
+ }
+
+ /**
+ * Check if the server is reachable.
+ */
+ public boolean isOnline() {
+ String url = apiBaseUrl + "/api/v1/health";
+
+ try {
+ Request request = new Request.Builder()
+ .url(url)
+ .header("X-Public-Key", publicKey)
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ return response.isSuccessful();
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private Map serializeEvent(TelemetryEvent event) {
+ Map result = new HashMap<>();
+ result.put("eventId", event.getEventId());
+ result.put("timestamp", formatInstant(event.getTimestamp()));
+ result.put("level", event.getLevel().getValue());
+ result.put("message", event.getMessage());
+ result.put("tags", event.getTags());
+ result.put("extra", event.getExtra());
+ result.put("environment", event.getEnvironment());
+ result.put("appVersion", event.getAppVersion());
+
+ // Breadcrumbs
+ List