From 3a5e4acffbbd7fd99aa976a8b0f493d438c105eb Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 8 May 2026 15:22:08 +0200 Subject: [PATCH 1/2] feat(telemetry): emit v1 schema fields (telemetry_type, profile, deployment_mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the #2007 contract (axonflow-enterprise) on top of v8.0. v8.0 shipped the .telemetry() builder removal + the stream classifier; this patch adds the three remaining v1 schema fields. Additive on the v8.0 line — no version bump. - telemetry_type: "sdk" discriminator field on every payload. - profile: from AXONFLOW_PROFILE env var, "unknown" when unset. - deployment_mode: aligned to v1 allowlist self_hosted | community_saas | unknown via the new classifyDeploymentMode (endpoint host + AXONFLOW_TRY=1 override). The prior config.Mode-based dimension is removed — deployment_mode now reflects topology only. New DeploymentMode constants class. - classifyEndpoint: drops the legacy "community-saas" branch and EndpointType.COMMUNITY_SAAS constant; topology lives on deployment_mode in v1. Tests: TelemetryReporterTest assertions migrated to the v1 schema + endpoint-derived deployment_mode (44 telemetry tests green). Signed-off-by: Saurabh Jain --- CHANGELOG.md | 5 ++ .../sdk/telemetry/TelemetryReporter.java | 73 +++++++++++++++++-- .../sdk/telemetry/TelemetryReporterTest.java | 29 ++++++-- 3 files changed, 94 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a35a47..3e3a34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,11 @@ contract — see `Removed` at the bottom of this entry for that. production heartbeat — see the checkpoint-service `IsValidIncomingStream` allowlist for the wire-side gate. +### Telemetry payload (v1 schema, axonflow-enterprise#2008) + +- New heartbeat fields: `telemetry_type: "sdk"`, `profile` (from `AXONFLOW_PROFILE`, `unknown` when unset), `deployment_mode` aligned to `self_hosted | community_saas | unknown` via the new `classifyDeploymentMode` (host + `AXONFLOW_TRY=1` override). New `DeploymentMode` constants class. +- `classifyEndpoint` no longer returns `community-saas` and `EndpointType.COMMUNITY_SAAS` is removed — that value moved off endpoint_type onto deployment_mode; analytics queries on the legacy value must update. + ## [7.1.0] - 2026-05-06 — X-Axonflow-Client header + scope-aware license validation **Companion release to platform v7.7.0.** The Java SDK now sends an diff --git a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java index 181b398..633a87d 100644 --- a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java +++ b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java @@ -119,6 +119,9 @@ public static boolean sendPingNow( (checkpointUrl != null && !checkpointUrl.isEmpty()) ? checkpointUrl : DEFAULT_ENDPOINT; String endpointType = classifyEndpoint(sdkEndpoint); + // v1 telemetry-schema: deployment_mode now derives from endpoint host + // (axonflow-enterprise#2008). config.Mode no longer drives this dimension. + String deploymentMode = classifyDeploymentMode(sdkEndpoint); try { long deadlineMs = @@ -133,7 +136,7 @@ public static boolean sendPingNow( ? detectPlatformVersion(sdkEndpoint, healthBudgetMs) : null; - String payload = buildPayload(mode, platformVersion, endpointType); + String payload = buildPayload(mode, platformVersion, endpointType, deploymentMode); long postBudgetMs = Math.max(0L, deadlineMs - System.nanoTime() / 1_000_000L); if (postBudgetMs < MIN_BUDGET_MS) { @@ -192,14 +195,25 @@ public static boolean isEnabled(String axonflowTelemetry) { /** Builds the JSON payload for the telemetry ping. */ static String buildPayload(String mode, String platformVersion) { - return buildPayload(mode, platformVersion, EndpointType.UNKNOWN); + return buildPayload(mode, platformVersion, EndpointType.UNKNOWN, DeploymentMode.UNKNOWN); } /** Builds the JSON payload with an explicit endpoint_type classification. */ static String buildPayload(String mode, String platformVersion, String endpointType) { + return buildPayload(mode, platformVersion, endpointType, DeploymentMode.UNKNOWN); + } + + /** + * Builds the JSON payload with explicit endpoint_type + deployment_mode classifications + * (v1 telemetry-schema, axonflow-enterprise#2008). + */ + static String buildPayload( + String mode, String platformVersion, String endpointType, String deploymentMode) { try { ObjectMapper mapper = new ObjectMapper(); ObjectNode root = mapper.createObjectNode(); + // v1 schema discriminator. Always "sdk" for this package. + root.put("telemetry_type", "sdk"); root.put("sdk", "java"); root.put("sdk_version", AxonFlowConfig.SDK_VERSION); if (platformVersion != null) { @@ -210,7 +224,10 @@ static String buildPayload(String mode, String platformVersion, String endpointT root.put("os", normalizeOS(System.getProperty("os.name"))); root.put("arch", normalizeArch(System.getProperty("os.arch"))); root.put("runtime_version", System.getProperty("java.version")); - root.put("deployment_mode", mode); + // v1 schema deployment_mode allowlist: self_hosted | community_saas | unknown. + // The prior config.Mode-based dimension is removed — deployment_mode now + // reflects deployment topology only (see classifyDeploymentMode). + root.put("deployment_mode", deploymentMode); root.put("endpoint_type", endpointType); ArrayNode features = mapper.createArrayNode(); @@ -218,6 +235,13 @@ static String buildPayload(String mode, String platformVersion, String endpointT root.put("instance_id", UUID.randomUUID().toString()); + // v1 schema profile dimension. Free-form deployment classifier sourced from + // AXONFLOW_PROFILE; "unknown" when unset. Analytics dimension only. + String profileEnv = System.getenv("AXONFLOW_PROFILE"); + String profile = + (profileEnv == null || profileEnv.trim().isEmpty()) ? "unknown" : profileEnv.trim(); + root.put("profile", profile); + // Stream classifier: sandbox-mode clients self-tag so analytics can distinguish dev/test // pings from production. Production-mode (and other modes) omit the field entirely so the // server defaults to "heartbeat" — preserving byte-identical wire shape relative to v7.x @@ -238,17 +262,57 @@ static String buildPayload(String mode, String platformVersion, String endpointT * Endpoint type classifications for telemetry. See issue #1525. * *

The raw URL is never sent to the checkpoint service — only the classification. + * + *

As of v8.0 the legacy {@code COMMUNITY_SAAS} value is removed — deployment topology + * lives on {@link DeploymentMode} per the v1 schema (axonflow-enterprise#2008). */ public static final class EndpointType { public static final String LOCALHOST = "localhost"; public static final String PRIVATE_NETWORK = "private_network"; public static final String REMOTE = "remote"; - public static final String COMMUNITY_SAAS = "community-saas"; public static final String UNKNOWN = "unknown"; private EndpointType() {} } + /** + * Deployment-mode classifications for telemetry (v1 schema, + * axonflow-enterprise#2008). Reflects deployment topology — distinct from + * the endpoint reachability classification on {@link EndpointType}. + */ + public static final class DeploymentMode { + public static final String SELF_HOSTED = "self_hosted"; + public static final String COMMUNITY_SAAS = "community_saas"; + public static final String UNKNOWN = "unknown"; + + private DeploymentMode() {} + } + + /** + * Classifies the configured AxonFlow endpoint into the v1 deployment-mode allowlist + * ({@code self_hosted | community_saas | unknown}). Community-SaaS detection fires on + * either an {@code *.try.getaxonflow.com} host or {@code AXONFLOW_TRY=1} (the explicit + * override path for tenants behind a custom hostname proxying try.getaxonflow.com). + * Empty/unparseable endpoint resolves to {@code unknown}. + */ + public static String classifyDeploymentMode(String url) { + if ("1".equals(System.getenv("AXONFLOW_TRY"))) return DeploymentMode.COMMUNITY_SAAS; + if (url == null || url.isEmpty()) return DeploymentMode.UNKNOWN; + String host; + try { + URI u = new URI(url); + host = u.getHost(); + if (host == null || host.isEmpty()) return DeploymentMode.UNKNOWN; + } catch (URISyntaxException e) { + return DeploymentMode.UNKNOWN; + } + host = host.toLowerCase(); + if ("try.getaxonflow.com".equals(host) || host.endsWith(".try.getaxonflow.com")) { + return DeploymentMode.COMMUNITY_SAAS; + } + return DeploymentMode.SELF_HOSTED; + } + private static final Pattern IPV4_PATTERN = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$"); @@ -261,7 +325,6 @@ private EndpointType() {} *

The raw URL is never sent — only the classification. */ public static String classifyEndpoint(String url) { - if ("1".equals(System.getenv("AXONFLOW_TRY"))) return EndpointType.COMMUNITY_SAAS; if (url == null || url.isEmpty()) { return EndpointType.UNKNOWN; } diff --git a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java index 7142533..6e46abc 100644 --- a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java +++ b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java @@ -66,6 +66,7 @@ void testPayloadFormat() throws Exception { String payload = TelemetryReporter.buildPayload("production", null); JsonNode root = objectMapper.readTree(payload); + assertThat(root.get("telemetry_type").asText()).isEqualTo("sdk"); assertThat(root.get("sdk").asText()).isEqualTo("java"); assertThat(root.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); assertThat(root.get("platform_version").isNull()).isTrue(); @@ -74,7 +75,9 @@ void testPayloadFormat() throws Exception { assertThat(root.get("arch").asText()) .isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch"))); assertThat(root.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version")); - assertThat(root.get("deployment_mode").asText()).isEqualTo("production"); + // v1 schema: 2-arg buildPayload defaults deployment_mode to "unknown". + assertThat(root.get("deployment_mode").asText()).isEqualTo("unknown"); + assertThat(root.get("profile").asText()).isEqualTo("unknown"); assertThat(root.get("features").isArray()).isTrue(); assertThat(root.get("features").size()).isEqualTo(0); assertThat(root.get("instance_id").asText()).isNotEmpty(); @@ -88,11 +91,14 @@ void testPayloadFormat() throws Exception { } @Test - @DisplayName("payload should reflect the given mode") - void testPayloadModeReflection() throws Exception { - String payload = TelemetryReporter.buildPayload("sandbox", null); + @DisplayName("payload deployment_mode reflects the v1 schema classifier output") + void testPayloadDeploymentModeReflection() throws Exception { + String payload = + TelemetryReporter.buildPayload( + "sandbox", null, TelemetryReporter.EndpointType.LOCALHOST, + TelemetryReporter.DeploymentMode.SELF_HOSTED); JsonNode root = objectMapper.readTree(payload); - assertThat(root.get("deployment_mode").asText()).isEqualTo("sandbox"); + assertThat(root.get("deployment_mode").asText()).isEqualTo("self_hosted"); } @Test @@ -152,9 +158,13 @@ void testCustomEndpoint(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { assertThat(requests).hasSize(1); JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); + assertThat(body.get("telemetry_type").asText()).isEqualTo("sdk"); assertThat(body.get("sdk").asText()).isEqualTo("java"); assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); - assertThat(body.get("deployment_mode").asText()).isEqualTo("production"); + // v1 schema: deployment_mode classifies from sdk endpoint host; localhost + // resolves to self_hosted (the v1 allowlist removes the production label). + assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted"); + assertThat(body.get("profile").asText()).isEqualTo("unknown"); assertThat(body.get("instance_id").asText()).isNotEmpty(); // production-mode payloads still omit stream on the wire. assertThat(body.has("stream")).isFalse(); @@ -250,7 +260,9 @@ void shouldFirePingWithStreamSandboxInSandboxMode(WireMockRuntimeInfo wmRuntimeI var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping"))); assertThat(requests).hasSize(1); JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); - assertThat(body.get("deployment_mode").asText()).isEqualTo("sandbox"); + // v1 schema: deployment_mode classifies from endpoint host (localhost -> + // self_hosted), NOT from config.Mode. The sandbox marker lives on `stream`. + assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted"); assertThat(body.get("stream")).isNotNull(); assertThat(body.get("stream").asText()).isEqualTo("sandbox"); } @@ -403,7 +415,8 @@ void testPayloadDeploymentModeEnterprise(WireMockRuntimeInfo wmRuntimeInfo) thro JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); assertThat(body.get("sdk").asText()).isEqualTo("java"); assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); - assertThat(body.get("deployment_mode").asText()).isEqualTo("enterprise"); + // v1 schema: deployment_mode is endpoint-derived; localhost -> self_hosted. + assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted"); assertThat(body.get("os").asText()) .isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name"))); assertThat(body.get("arch").asText()) From 572fbfae0e71a38cba157f7da489e1642529afbd Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 8 May 2026 15:36:27 +0200 Subject: [PATCH 2/2] chore: trigger CI re-run after [skip-runtime-e2e] title edit Signed-off-by: Saurabh Jain