Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.8.0] - 2026-03-03

### Added

- `healthCheck()` now returns `capabilities` list and `sdkCompatibility` in `HealthStatus`
- `hasCapability(name)` method on `HealthStatus` to check if platform supports a specific feature
- `SDK_VERSION` constant on `AxonFlowConfig` for programmatic SDK version access
- User-Agent header corrected from `axonflow-java-sdk/1.0.0` to `axonflow-sdk-java/{version}`
- Version mismatch warning logged when SDK version is below platform's `min_sdk_version`
- `PlatformCapability` and `SDKCompatibility` types
- `traceId` field on `CreateWorkflowRequest`, `CreateWorkflowResponse`, `WorkflowStatusResponse`, and `ListWorkflowsOptions` for distributed tracing correlation
- `ToolContext` type for per-tool governance within workflow steps
- `toolContext` field on `StepGateRequest` for tool-level policy enforcement
- `listWorkflows()` now supports `traceId` filter parameter
- Anonymous runtime telemetry for version adoption tracking and feature usage signals
- `TelemetryEnabled` / `telemetry` configuration option to explicitly control telemetry
- `AXONFLOW_TELEMETRY=off` and `DO_NOT_TRACK=1` environment variable opt-out support

### Fixed

- Default User-Agent was hardcoded to `1.0.0` regardless of actual SDK version

---

## [3.7.0] - 2026-02-28

### Added
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,12 @@ MCPQueryResponse resp = client.queryConnector(query);

For enterprise features, contact [sales@getaxonflow.com](mailto:sales@getaxonflow.com).

## Telemetry

This SDK sends anonymous usage telemetry (SDK version, OS, enabled features) to help improve AxonFlow.
No prompts, payloads, or PII are ever collected. Opt out: `AXONFLOW_TELEMETRY=off` or `DO_NOT_TRACK=1`.
See [Telemetry Documentation](https://docs.getaxonflow.com/docs/telemetry) for full details.

## Contributing

We welcome contributions. Please see our [Contributing Guide](CONTRIBUTING.md) for details.
Expand Down
62 changes: 60 additions & 2 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.getaxonflow.sdk;

import com.getaxonflow.sdk.exceptions.*;
import com.getaxonflow.sdk.telemetry.TelemetryReporter;
import com.getaxonflow.sdk.types.*;
import com.getaxonflow.sdk.types.codegovernance.*;
import com.getaxonflow.sdk.types.costcontrols.CostControlTypes.*;
Expand Down Expand Up @@ -138,6 +139,17 @@ private AxonFlow(AxonFlowConfig config) {
this.masfeatNamespace = new MASFEATNamespace();

logger.info("AxonFlow client initialized for {}", config.getEndpoint());

// Send telemetry ping (fire-and-forget).
boolean hasCredentials = config.getClientId() != null && !config.getClientId().isEmpty()
&& config.getClientSecret() != null && !config.getClientSecret().isEmpty();
TelemetryReporter.sendPing(
config.getMode() != null ? config.getMode().getValue() : "production",
config.getEndpoint(),
config.getTelemetry(),
config.isDebug(),
hasCredentials
);
}

private static ObjectMapper createObjectMapper() {
Expand All @@ -149,6 +161,40 @@ private static ObjectMapper createObjectMapper() {
return mapper;
}

/**
* Compares two semantic version strings numerically (major.minor.patch).
* Returns negative if a < b, zero if equal, positive if a > b.
*/
private static int compareSemver(String a, String b) {
String[] partsA = a.split("\\.");
String[] partsB = b.split("\\.");
int length = Math.max(partsA.length, partsB.length);
for (int i = 0; i < length; i++) {
int numA = 0;
int numB = 0;
if (i < partsA.length) {
try {
String cleanA = partsA[i].contains("-") ? partsA[i].substring(0, partsA[i].indexOf("-")) : partsA[i];
numA = Integer.parseInt(cleanA);
} catch (NumberFormatException ignored) {
// default to 0
}
}
if (i < partsB.length) {
try {
String cleanB = partsB[i].contains("-") ? partsB[i].substring(0, partsB[i].indexOf("-")) : partsB[i];
numB = Integer.parseInt(cleanB);
} catch (NumberFormatException ignored) {
// default to 0
}
}
if (numA != numB) {
return Integer.compare(numA, numB);
}
}
return 0;
}

// ========================================================================
// Factory Methods
// ========================================================================
Expand Down Expand Up @@ -206,12 +252,21 @@ public static AxonFlow sandbox(String agentUrl) {
* @throws ConnectionException if the Agent cannot be reached
*/
public HealthStatus healthCheck() {
return retryExecutor.execute(() -> {
HealthStatus status = retryExecutor.execute(() -> {
Request request = buildRequest("GET", "/health", null);
try (Response response = httpClient.newCall(request).execute()) {
return parseResponse(response, HealthStatus.class);
}
}, "healthCheck");

if (status.getSdkCompatibility() != null
&& status.getSdkCompatibility().getMinSdkVersion() != null
&& compareSemver(AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion()) < 0) {
logger.warn("SDK version {} is below minimum supported version {}. Please upgrade.",
AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion());
}

return status;
}

/**
Expand Down Expand Up @@ -265,7 +320,7 @@ public HealthStatus orchestratorHealthCheck() {
Request httpRequest = buildOrchestratorRequest("GET", "/health", null);
try (Response response = httpClient.newCall(httpRequest).execute()) {
if (!response.isSuccessful()) {
return new HealthStatus("unhealthy", null, null, null);
return new HealthStatus("unhealthy", null, null, null, null, null);
}
return parseResponse(response, HealthStatus.class);
}
Expand Down Expand Up @@ -4303,6 +4358,9 @@ public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse li
if (options.getOffset() > 0) {
appendQueryParam(query, "offset", String.valueOf(options.getOffset()));
}
if (options.getTraceId() != null) {
appendQueryParam(query, "trace_id", options.getTraceId());
}
}

if (query.length() > 0) {
Expand Down
37 changes: 36 additions & 1 deletion src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
*/
public final class AxonFlowConfig {

/** SDK version string. */
public static final String SDK_VERSION = "3.8.0";

/** Default timeout for HTTP requests. */
public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(60);

Expand All @@ -58,6 +61,7 @@ public final class AxonFlowConfig {
private final RetryConfig retryConfig;
private final CacheConfig cacheConfig;
private final String userAgent;
private final Boolean telemetry;

private AxonFlowConfig(Builder builder) {
this.endpoint = normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT);
Expand All @@ -69,7 +73,8 @@ private AxonFlowConfig(Builder builder) {
this.insecureSkipVerify = builder.insecureSkipVerify;
this.retryConfig = builder.retryConfig != null ? builder.retryConfig : RetryConfig.defaults();
this.cacheConfig = builder.cacheConfig != null ? builder.cacheConfig : CacheConfig.defaults();
this.userAgent = builder.userAgent != null ? builder.userAgent : "axonflow-java-sdk/1.0.0";
this.userAgent = builder.userAgent != null ? builder.userAgent : "axonflow-sdk-java/" + SDK_VERSION;
this.telemetry = builder.telemetry;

validate();
}
Expand Down Expand Up @@ -208,6 +213,18 @@ public String getUserAgent() {
return userAgent;
}

/**
* Returns the telemetry config override.
*
* <p>{@code null} means use the default behavior (ON for production, OFF for sandbox).
* {@code Boolean.TRUE} forces telemetry on, {@code Boolean.FALSE} forces it off.
*
* @return the telemetry override, or null for default behavior
*/
public Boolean getTelemetry() {
return telemetry;
}

public static Builder builder() {
return new Builder();
}
Expand Down Expand Up @@ -254,6 +271,7 @@ public static final class Builder {
private RetryConfig retryConfig;
private CacheConfig cacheConfig;
private String userAgent;
private Boolean telemetry;

private Builder() {}

Expand Down Expand Up @@ -386,6 +404,23 @@ public Builder userAgent(String userAgent) {
return this;
}

/**
* Sets the telemetry override.
*
* <p>{@code null} (default) uses the mode-based default: ON for production, OFF for sandbox.
* {@code Boolean.TRUE} forces telemetry on, {@code Boolean.FALSE} forces it off.
*
* <p>Telemetry can also be disabled globally via environment variables:
* {@code DO_NOT_TRACK=1} or {@code AXONFLOW_TELEMETRY=off}.
*
* @param telemetry true to enable, false to disable, null for default behavior
* @return this builder
*/
public Builder telemetry(Boolean telemetry) {
this.telemetry = telemetry;
return this;
}

/**
* Builds the configuration.
*
Expand Down
Loading
Loading