Skip to content

Commit f27c26b

Browse files
feat(telemetry): emit v1 schema fields [skip-runtime-e2e] (#169)
* feat(telemetry): emit v1 schema fields (telemetry_type, profile, deployment_mode) 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 <saurabhjain1592@gmail.com> * chore: trigger CI re-run after [skip-runtime-e2e] title edit Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com> --------- Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com>
1 parent b70806a commit f27c26b

3 files changed

Lines changed: 94 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ contract — see `Removed` at the bottom of this entry for that.
6060
production heartbeat — see the checkpoint-service
6161
`IsValidIncomingStream` allowlist for the wire-side gate.
6262

63+
### Telemetry payload (v1 schema, axonflow-enterprise#2008)
64+
65+
- 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.
66+
- `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.
67+
6368
## [7.1.0] - 2026-05-06 — X-Axonflow-Client header + scope-aware license validation
6469

6570
**Companion release to platform v7.7.0.** The Java SDK now sends an

src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ public static boolean sendPingNow(
119119
(checkpointUrl != null && !checkpointUrl.isEmpty()) ? checkpointUrl : DEFAULT_ENDPOINT;
120120

121121
String endpointType = classifyEndpoint(sdkEndpoint);
122+
// v1 telemetry-schema: deployment_mode now derives from endpoint host
123+
// (axonflow-enterprise#2008). config.Mode no longer drives this dimension.
124+
String deploymentMode = classifyDeploymentMode(sdkEndpoint);
122125

123126
try {
124127
long deadlineMs =
@@ -133,7 +136,7 @@ public static boolean sendPingNow(
133136
? detectPlatformVersion(sdkEndpoint, healthBudgetMs)
134137
: null;
135138

136-
String payload = buildPayload(mode, platformVersion, endpointType);
139+
String payload = buildPayload(mode, platformVersion, endpointType, deploymentMode);
137140

138141
long postBudgetMs = Math.max(0L, deadlineMs - System.nanoTime() / 1_000_000L);
139142
if (postBudgetMs < MIN_BUDGET_MS) {
@@ -192,14 +195,25 @@ public static boolean isEnabled(String axonflowTelemetry) {
192195

193196
/** Builds the JSON payload for the telemetry ping. */
194197
static String buildPayload(String mode, String platformVersion) {
195-
return buildPayload(mode, platformVersion, EndpointType.UNKNOWN);
198+
return buildPayload(mode, platformVersion, EndpointType.UNKNOWN, DeploymentMode.UNKNOWN);
196199
}
197200

198201
/** Builds the JSON payload with an explicit endpoint_type classification. */
199202
static String buildPayload(String mode, String platformVersion, String endpointType) {
203+
return buildPayload(mode, platformVersion, endpointType, DeploymentMode.UNKNOWN);
204+
}
205+
206+
/**
207+
* Builds the JSON payload with explicit endpoint_type + deployment_mode classifications
208+
* (v1 telemetry-schema, axonflow-enterprise#2008).
209+
*/
210+
static String buildPayload(
211+
String mode, String platformVersion, String endpointType, String deploymentMode) {
200212
try {
201213
ObjectMapper mapper = new ObjectMapper();
202214
ObjectNode root = mapper.createObjectNode();
215+
// v1 schema discriminator. Always "sdk" for this package.
216+
root.put("telemetry_type", "sdk");
203217
root.put("sdk", "java");
204218
root.put("sdk_version", AxonFlowConfig.SDK_VERSION);
205219
if (platformVersion != null) {
@@ -210,14 +224,24 @@ static String buildPayload(String mode, String platformVersion, String endpointT
210224
root.put("os", normalizeOS(System.getProperty("os.name")));
211225
root.put("arch", normalizeArch(System.getProperty("os.arch")));
212226
root.put("runtime_version", System.getProperty("java.version"));
213-
root.put("deployment_mode", mode);
227+
// v1 schema deployment_mode allowlist: self_hosted | community_saas | unknown.
228+
// The prior config.Mode-based dimension is removed — deployment_mode now
229+
// reflects deployment topology only (see classifyDeploymentMode).
230+
root.put("deployment_mode", deploymentMode);
214231
root.put("endpoint_type", endpointType);
215232

216233
ArrayNode features = mapper.createArrayNode();
217234
root.set("features", features);
218235

219236
root.put("instance_id", UUID.randomUUID().toString());
220237

238+
// v1 schema profile dimension. Free-form deployment classifier sourced from
239+
// AXONFLOW_PROFILE; "unknown" when unset. Analytics dimension only.
240+
String profileEnv = System.getenv("AXONFLOW_PROFILE");
241+
String profile =
242+
(profileEnv == null || profileEnv.trim().isEmpty()) ? "unknown" : profileEnv.trim();
243+
root.put("profile", profile);
244+
221245
// Stream classifier: sandbox-mode clients self-tag so analytics can distinguish dev/test
222246
// pings from production. Production-mode (and other modes) omit the field entirely so the
223247
// 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
238262
* Endpoint type classifications for telemetry. See issue #1525.
239263
*
240264
* <p>The raw URL is never sent to the checkpoint service — only the classification.
265+
*
266+
* <p>As of v8.0 the legacy {@code COMMUNITY_SAAS} value is removed — deployment topology
267+
* lives on {@link DeploymentMode} per the v1 schema (axonflow-enterprise#2008).
241268
*/
242269
public static final class EndpointType {
243270
public static final String LOCALHOST = "localhost";
244271
public static final String PRIVATE_NETWORK = "private_network";
245272
public static final String REMOTE = "remote";
246-
public static final String COMMUNITY_SAAS = "community-saas";
247273
public static final String UNKNOWN = "unknown";
248274

249275
private EndpointType() {}
250276
}
251277

278+
/**
279+
* Deployment-mode classifications for telemetry (v1 schema,
280+
* axonflow-enterprise#2008). Reflects deployment topology — distinct from
281+
* the endpoint reachability classification on {@link EndpointType}.
282+
*/
283+
public static final class DeploymentMode {
284+
public static final String SELF_HOSTED = "self_hosted";
285+
public static final String COMMUNITY_SAAS = "community_saas";
286+
public static final String UNKNOWN = "unknown";
287+
288+
private DeploymentMode() {}
289+
}
290+
291+
/**
292+
* Classifies the configured AxonFlow endpoint into the v1 deployment-mode allowlist
293+
* ({@code self_hosted | community_saas | unknown}). Community-SaaS detection fires on
294+
* either an {@code *.try.getaxonflow.com} host or {@code AXONFLOW_TRY=1} (the explicit
295+
* override path for tenants behind a custom hostname proxying try.getaxonflow.com).
296+
* Empty/unparseable endpoint resolves to {@code unknown}.
297+
*/
298+
public static String classifyDeploymentMode(String url) {
299+
if ("1".equals(System.getenv("AXONFLOW_TRY"))) return DeploymentMode.COMMUNITY_SAAS;
300+
if (url == null || url.isEmpty()) return DeploymentMode.UNKNOWN;
301+
String host;
302+
try {
303+
URI u = new URI(url);
304+
host = u.getHost();
305+
if (host == null || host.isEmpty()) return DeploymentMode.UNKNOWN;
306+
} catch (URISyntaxException e) {
307+
return DeploymentMode.UNKNOWN;
308+
}
309+
host = host.toLowerCase();
310+
if ("try.getaxonflow.com".equals(host) || host.endsWith(".try.getaxonflow.com")) {
311+
return DeploymentMode.COMMUNITY_SAAS;
312+
}
313+
return DeploymentMode.SELF_HOSTED;
314+
}
315+
252316
private static final Pattern IPV4_PATTERN =
253317
Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$");
254318

@@ -261,7 +325,6 @@ private EndpointType() {}
261325
* <p>The raw URL is never sent — only the classification.
262326
*/
263327
public static String classifyEndpoint(String url) {
264-
if ("1".equals(System.getenv("AXONFLOW_TRY"))) return EndpointType.COMMUNITY_SAAS;
265328
if (url == null || url.isEmpty()) {
266329
return EndpointType.UNKNOWN;
267330
}

src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void testPayloadFormat() throws Exception {
6666
String payload = TelemetryReporter.buildPayload("production", null);
6767
JsonNode root = objectMapper.readTree(payload);
6868

69+
assertThat(root.get("telemetry_type").asText()).isEqualTo("sdk");
6970
assertThat(root.get("sdk").asText()).isEqualTo("java");
7071
assertThat(root.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION);
7172
assertThat(root.get("platform_version").isNull()).isTrue();
@@ -74,7 +75,9 @@ void testPayloadFormat() throws Exception {
7475
assertThat(root.get("arch").asText())
7576
.isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch")));
7677
assertThat(root.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version"));
77-
assertThat(root.get("deployment_mode").asText()).isEqualTo("production");
78+
// v1 schema: 2-arg buildPayload defaults deployment_mode to "unknown".
79+
assertThat(root.get("deployment_mode").asText()).isEqualTo("unknown");
80+
assertThat(root.get("profile").asText()).isEqualTo("unknown");
7881
assertThat(root.get("features").isArray()).isTrue();
7982
assertThat(root.get("features").size()).isEqualTo(0);
8083
assertThat(root.get("instance_id").asText()).isNotEmpty();
@@ -88,11 +91,14 @@ void testPayloadFormat() throws Exception {
8891
}
8992

9093
@Test
91-
@DisplayName("payload should reflect the given mode")
92-
void testPayloadModeReflection() throws Exception {
93-
String payload = TelemetryReporter.buildPayload("sandbox", null);
94+
@DisplayName("payload deployment_mode reflects the v1 schema classifier output")
95+
void testPayloadDeploymentModeReflection() throws Exception {
96+
String payload =
97+
TelemetryReporter.buildPayload(
98+
"sandbox", null, TelemetryReporter.EndpointType.LOCALHOST,
99+
TelemetryReporter.DeploymentMode.SELF_HOSTED);
94100
JsonNode root = objectMapper.readTree(payload);
95-
assertThat(root.get("deployment_mode").asText()).isEqualTo("sandbox");
101+
assertThat(root.get("deployment_mode").asText()).isEqualTo("self_hosted");
96102
}
97103

98104
@Test
@@ -152,9 +158,13 @@ void testCustomEndpoint(WireMockRuntimeInfo wmRuntimeInfo) throws Exception {
152158
assertThat(requests).hasSize(1);
153159

154160
JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString());
161+
assertThat(body.get("telemetry_type").asText()).isEqualTo("sdk");
155162
assertThat(body.get("sdk").asText()).isEqualTo("java");
156163
assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION);
157-
assertThat(body.get("deployment_mode").asText()).isEqualTo("production");
164+
// v1 schema: deployment_mode classifies from sdk endpoint host; localhost
165+
// resolves to self_hosted (the v1 allowlist removes the production label).
166+
assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted");
167+
assertThat(body.get("profile").asText()).isEqualTo("unknown");
158168
assertThat(body.get("instance_id").asText()).isNotEmpty();
159169
// production-mode payloads still omit stream on the wire.
160170
assertThat(body.has("stream")).isFalse();
@@ -250,7 +260,9 @@ void shouldFirePingWithStreamSandboxInSandboxMode(WireMockRuntimeInfo wmRuntimeI
250260
var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping")));
251261
assertThat(requests).hasSize(1);
252262
JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString());
253-
assertThat(body.get("deployment_mode").asText()).isEqualTo("sandbox");
263+
// v1 schema: deployment_mode classifies from endpoint host (localhost ->
264+
// self_hosted), NOT from config.Mode. The sandbox marker lives on `stream`.
265+
assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted");
254266
assertThat(body.get("stream")).isNotNull();
255267
assertThat(body.get("stream").asText()).isEqualTo("sandbox");
256268
}
@@ -403,7 +415,8 @@ void testPayloadDeploymentModeEnterprise(WireMockRuntimeInfo wmRuntimeInfo) thro
403415
JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString());
404416
assertThat(body.get("sdk").asText()).isEqualTo("java");
405417
assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION);
406-
assertThat(body.get("deployment_mode").asText()).isEqualTo("enterprise");
418+
// v1 schema: deployment_mode is endpoint-derived; localhost -> self_hosted.
419+
assertThat(body.get("deployment_mode").asText()).isEqualTo("self_hosted");
407420
assertThat(body.get("os").asText())
408421
.isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name")));
409422
assertThat(body.get("arch").asText())

0 commit comments

Comments
 (0)