Skip to content
Draft
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,18 @@ public class Sample {
}
```

### SDK telemetry

By default, the library sends anonymous usage telemetry to Chargebee. This helps us improve the SDK and API.

You can disable this behavior if you prefer:

```java
ChargebeeClient client = ChargebeeClient.builder(apiKey, site)
.sdkTelemetryEnabled(false)
.build();
```

### Telemetry (OpenTelemetry)

Optional. Pass a `telemetryAdapter` when you want Chargebee API calls traced in your observability stack (Datadog, Splunk, Honeycomb, Jaeger, etc.). OpenTelemetry is not bundled with `chargebee-java` — add and configure it in your app, implement `TelemetryAdapter`, and wire it on the client.
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/com/chargebee/v4/client/ChargebeeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.chargebee.v4.exceptions.TimeoutException;
import com.chargebee.v4.exceptions.TransportException;
import com.chargebee.v4.internal.RetryConfig;
import com.chargebee.v4.telemetry.SdkTelemetryState;
import com.chargebee.v4.telemetry.TelemetryAdapter;
import com.chargebee.v4.telemetry.TelemetryExecutor;
import com.chargebee.v4.transport.*;
Expand Down Expand Up @@ -43,6 +44,8 @@ public final class ChargebeeClient extends ClientMethodsImpl implements AutoClos
private final RequestInterceptor requestInterceptor;
private final RequestContext clientHeaders;
private final TelemetryAdapter telemetryAdapter;
private final boolean sdkTelemetryEnabled;
private final SdkTelemetryState sdkTelemetryState = new SdkTelemetryState();
private final ScheduledExecutorService retryScheduler;

// Auto-generated service registry for lazy loading
Expand All @@ -61,6 +64,7 @@ private ChargebeeClient(Builder builder) {
this.requestInterceptor = builder.requestInterceptor;
this.clientHeaders = new RequestContext(builder.clientHeaders.getHeaders());
this.telemetryAdapter = builder.telemetryAdapter;
this.sdkTelemetryEnabled = builder.sdkTelemetryEnabled;
this.retryScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "chargebee-retry-scheduler");
t.setDaemon(true);
Expand Down Expand Up @@ -97,6 +101,10 @@ public static Builder builder(String apiKey, String siteName) {
public RequestInterceptor getRequestInterceptor() { return requestInterceptor; }
public RequestContext getClientHeaders() { return clientHeaders; }
public TelemetryAdapter getTelemetryAdapter() { return telemetryAdapter; }
public boolean isSdkTelemetryEnabled() { return sdkTelemetryEnabled; }

/** Internal SDK telemetry state; not part of the supported public API. */
public SdkTelemetryState getSdkTelemetryState() { return sdkTelemetryState; }

public String getSdkVersion() {
return getVersion();
Expand Down Expand Up @@ -577,6 +585,7 @@ public static final class Builder {
private String protocol = "https";
private RequestInterceptor requestInterceptor;
private TelemetryAdapter telemetryAdapter;
private boolean sdkTelemetryEnabled = true;
private final RequestContext clientHeaders = new RequestContext();

private Builder() {}
Expand All @@ -600,6 +609,13 @@ public Builder timeout(int connectTimeoutMs, int readTimeoutMs) {
public Builder protocol(String protocol) { this.protocol = protocol; return this; }
public Builder requestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptor = requestInterceptor; return this; }
public Builder telemetryAdapter(TelemetryAdapter telemetryAdapter) { this.telemetryAdapter = telemetryAdapter; return this; }
/**
* Enables the anonymous SDK telemetry request header, on by default. It carries SDK name,
* version, runtime, and the resource/operation/latency/status of the previous call on this
* client. It never carries request or response payloads. Pass {@code false} to opt out;
* this is independent of {@link #telemetryAdapter(TelemetryAdapter)}.
*/
public Builder sdkTelemetryEnabled(boolean sdkTelemetryEnabled) { this.sdkTelemetryEnabled = sdkTelemetryEnabled; return this; }

// Header helpers
public Builder header(String name, String value) {
Expand Down
103 changes: 103 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

import com.chargebee.v4.client.ChargebeeClient;
import com.chargebee.v4.transport.DefaultTransport;
import com.chargebee.v4.transport.Request;
import com.chargebee.v4.transport.Response;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Emits the anonymous SDK telemetry request header, independently of any customer telemetry
* adapter.
*
* <p>On the first API call of a client instance, attach {@code f;…} with enabled feature codes when
* any are present; omit the header when none are enabled. Later calls on the same client never
* attach again. SDK identity is correlated via {@code User-Agent}. Every failure path is swallowed
* and logged at {@code WARNING}: telemetry must never fail an API call.
*/
final class SdkTelemetryEmitter {

private static final Logger LOGGER = Logger.getLogger(SdkTelemetryEmitter.class.getName());

private SdkTelemetryEmitter() {}

/** Attaches the one-shot features header when applicable, then invokes {@code next}. */
static Response around(
ChargebeeClient client, Request request, Function<Request, Response> next) {
if (!client.isSdkTelemetryEnabled()) {
return next.apply(request);
}
return next.apply(attachHeader(client, request));
}

/** Async variant of {@link #around}. */
static CompletableFuture<Response> aroundAsync(
ChargebeeClient client,
Request request,
Function<Request, CompletableFuture<Response>> next) {
if (!client.isSdkTelemetryEnabled()) {
return next.apply(request);
}
return next.apply(attachHeader(client, request));
}

/** Returns {@code request} with the telemetry header, or {@code request} unchanged. */
private static Request attachHeader(ChargebeeClient client, Request request) {
try {
if (!client.getSdkTelemetryState().tryMarkEmitted()) {
return request;
}
String headerValue = SdkTelemetryHeaderBuilder.build(resolveFeatures(client, request));
if (headerValue == null) {
return request;
}
return request.withHeader(SdkTelemetryHeader.HEADER_NAME, headerValue);
} catch (Exception err) {
logSuppressed("attach header", err);
return request;
}
}

/** Collects enabled feature codes for the current client/request configuration. */
private static Set<SdkTelemetryFeature> resolveFeatures(ChargebeeClient client, Request request) {
Set<SdkTelemetryFeature> features = new LinkedHashSet<>();
if (TelemetryAdapterExecutor.resolveAdapter(client, request) != null) {
features.add(SdkTelemetryFeature.TELEMETRY_ADAPTER);
}
if (!(client.getTransport() instanceof DefaultTransport)) {
features.add(SdkTelemetryFeature.CUSTOM_TRANSPORT);
}
if (isRetryConfigActive(client, request)) {
features.add(SdkTelemetryFeature.RETRY_CONFIG);
}
return features;
}

/** Mirrors how {@code sendWithRetryInternal} decides whether retry configuration is in play. */
private static boolean isRetryConfigActive(ChargebeeClient client, Request request) {
if (request.getMaxNetworkRetriesOverride() != null) {
return true;
}
return client.getRetry() != null && client.getRetry().isEnabled();
}

/** Logs a suppressed telemetry failure without affecting the API call. */
private static void logSuppressed(String step, Exception err) {
LOGGER.log(
Level.WARNING,
"SDK telemetry could not " + step + " (" + err.getMessage() + "); API call unaffected.",
err);
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryFeature.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

/**
* SDK configuration features reported under the {@code f} segment of {@link
* SdkTelemetryHeader#HEADER_NAME}. Wire codes are maintained in sdk-generator.
*/
enum SdkTelemetryFeature {
/** Customer {@link TelemetryAdapter} configured. */
TELEMETRY_ADAPTER("ta"),

/** Non-default transport configured. */
CUSTOM_TRANSPORT("ct"),

/** Retries enabled on the client or request. */
RETRY_CONFIG("rc");

private final String code;

SdkTelemetryFeature(String code) {
this.code = code;
}

/** Two-letter wire code appended as a boolean sf-param on {@code f}. */
String code() {
return code;
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryHeader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

/** Constants for the anonymous SDK telemetry request header. */
public final class SdkTelemetryHeader {

/**
* Name of the request header carrying SDK telemetry. Exposed so that proxies, interceptors, and
* tests can reference it without hardcoding the string.
*/
public static final String HEADER_NAME = "x-chargebee-sdk-telemetry";

/**
* Server drops larger values, so the SDK omits the header rather than sending a truncated one.
*/
static final int MAX_HEADER_BYTES = 4096;

/** RFC 9651 sf-list item name for the features segment. */
static final String FEATURES_KEY = "f";

private SdkTelemetryHeader() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

import java.nio.charset.StandardCharsets;
import java.util.Collection;

/**
* Builds RFC 9651 values for {@link SdkTelemetryHeader#HEADER_NAME}: a features segment keyed by
* {@link SdkTelemetryHeader#FEATURES_KEY} with enabled feature codes as boolean params (for example
* {@code f;ta;rc}). Correlate SDK identity via {@code User-Agent}.
*/
final class SdkTelemetryHeaderBuilder {

private SdkTelemetryHeaderBuilder() {}

/**
* Returns {@code null} when {@code features} is empty/null or the value exceeds {@link
* SdkTelemetryHeader#MAX_HEADER_BYTES}.
*/
static String build(Collection<SdkTelemetryFeature> features) {
if (features == null || features.isEmpty()) {
return null;
}

StringBuilder value = new StringBuilder(SdkTelemetryHeader.FEATURES_KEY);
for (SdkTelemetryFeature feature : features) {
if (feature == null) {
continue;
}
value.append(';').append(feature.code());
}

if (value.length() == SdkTelemetryHeader.FEATURES_KEY.length()) {
return null;
}

String headerValue = value.toString();
if (headerValue.getBytes(StandardCharsets.UTF_8).length > SdkTelemetryHeader.MAX_HEADER_BYTES) {
return null;
}
return headerValue;
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/chargebee/v4/telemetry/SdkTelemetryState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* This file is auto-generated by Chargebee.
* For more information on how to make changes to this file, please see the README.
* Reach out to dx@chargebee.com for any questions.
* Copyright 2026 Chargebee Inc.
*/

package com.chargebee.v4.telemetry;

import java.util.concurrent.atomic.AtomicBoolean;

/**
* Per-client gate so the SDK telemetry header is considered at most once per client instance.
*
* <p>Internal SDK type: applications must not depend on it. It is public only so that {@code
* ChargebeeClient} can own one instance; all accessors are package-private.
*/
public final class SdkTelemetryState {

private final AtomicBoolean emitted = new AtomicBoolean(false);

/**
* Claims the single emission slot for this client. Returns {@code true} only for the first
* caller.
*/
boolean tryMarkEmitted() {
return emitted.compareAndSet(false, true);
}

/** Whether this client has already considered emitting the telemetry header. */
boolean hasEmitted() {
return emitted.get();
}

/** Clears the emission gate (tests only). */
void clear() {
emitted.set(false);
}
}
Loading
Loading