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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Remember that each namespace requires its own authentication token type as descr
* [Server Selection](#server-selection)
* [Custom HTTP Client](#custom-http-client)
* [Debugging](#debugging)
* [Experimental Features and Deprecation Testing](#experimental-features-and-deprecation-testing)
* [Development](#development)
* [Maturity](#maturity)
* [Contributions](#contributions)
Expand Down Expand Up @@ -1598,6 +1599,55 @@ __NOTE__: This is a convenience method that calls `HTTPClient.enableDebugLogging
Another option is to set the System property `-Djdk.httpclient.HttpClient.log=all`. However, this second option does not log bodies.
<!-- End Debugging [debug] -->

## Experimental Features and Deprecation Testing

The SDK provides options to test upcoming API changes before they become the default behavior. This is useful for:

- **Testing experimental features** before they are generally available
- **Preparing for deprecations** by excluding deprecated endpoints ahead of their removal

### Configuration Options

You can configure these options either via environment variables or SDK constructor options:

#### Using Environment Variables

```bash
# Set environment variables before running your application
export X_GLEAN_EXCLUDE_DEPRECATED_AFTER="2026-10-15"
export X_GLEAN_INCLUDE_EXPERIMENTAL="true"
```

```java
// Environment variables are automatically read by the SDK
Glean glean = Glean.builder()
.apiToken(System.getenv("GLEAN_API_TOKEN"))
.instance("instance-name")
.build();
```

#### Using SDK Constructor Options

```java
Glean glean = Glean.builder()
.apiToken(System.getenv("GLEAN_API_TOKEN"))
.instance("instance-name")
.excludeDeprecatedAfter("2026-10-15")
.includeExperimental(true)
.build();
```

### Option Reference

| Option | Environment Variable | Type | Description |
| ------ | -------------------- | ---- | ----------- |
| `excludeDeprecatedAfter` | `X_GLEAN_EXCLUDE_DEPRECATED_AFTER` | `String` (date) | Exclude API endpoints that will be deprecated after this date (format: `YYYY-MM-DD`). Use this to test your integration against upcoming deprecations. |
| `includeExperimental` | `X_GLEAN_INCLUDE_EXPERIMENTAL` | `boolean` | When `true`, enables experimental API features that are not yet generally available. Use this to preview and test new functionality. |

> **Note:** Environment variables take precedence over SDK constructor options when both are set.

> **Warning:** Experimental features may change or be removed without notice. Do not rely on experimental features in production environments.

<!-- Placeholder for Future Speakeasy SDK Sections -->

# Development
Expand Down
11 changes: 10 additions & 1 deletion build-extras.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
// This file
// This file
// * is referred to in an `apply from` command in `build.gradle`
// * can be used to customise `build.gradle`
// * is generated once and not overwritten in SDK generation updates

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

test {
useJUnitPlatform()
}
29 changes: 29 additions & 0 deletions src/main/java/com/glean/api_client/glean_api_client/Glean.java
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,35 @@ public Builder instance(String instance) {

return this;
}

/**
* Exclude API endpoints that will be deprecated after this date.
* Use this to test your integration against upcoming deprecations.
*
* <p>More information: <a href="https://developers.glean.com/deprecations/overview">Deprecations Overview</a>
*
* @param excludeDeprecatedAfter date string in YYYY-MM-DD format (e.g., '2026-10-15')
* @return The builder instance.
*/
public Builder excludeDeprecatedAfter(String excludeDeprecatedAfter) {
this.sdkConfiguration.setExcludeDeprecatedAfter(Optional.of(excludeDeprecatedAfter));
return this;
}

/**
* Enable experimental API features that are not yet generally available.
* Use this to preview and test new functionality.
*
* <p><strong>Warning:</strong> Experimental features may change or be removed without notice.
* Do not rely on experimental features in production environments.
*
* @param includeExperimental whether to include experimental features
* @return The builder instance.
*/
public Builder includeExperimental(boolean includeExperimental) {
this.sdkConfiguration.setIncludeExperimental(Optional.of(includeExperimental));
return this;
}
// Visible for testing, may be accessed via reflection in tests
Builder _hooks(com.glean.api_client.glean_api_client.utils.Hooks hooks) {
sdkConfiguration.setHooks(hooks);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public Map<String, String> getServerVariableDefaults() {
return serverVariables.get(this.serverIdx);
}
private Optional<RetryConfig> retryConfig = Optional.empty();

public Optional<RetryConfig> retryConfig() {
return retryConfig;
}
Expand All @@ -137,6 +137,50 @@ public void setRetryConfig(Optional<RetryConfig> retryConfig) {
Utils.checkNotNull(retryConfig, "retryConfig");
this.retryConfig = retryConfig;
}

private Optional<String> excludeDeprecatedAfter = Optional.empty();

/**
* Gets the date after which deprecated API endpoints should be excluded.
*
* @return Optional containing the date string (YYYY-MM-DD format) if set
*/
public Optional<String> excludeDeprecatedAfter() {
return excludeDeprecatedAfter;
}

/**
* Sets the date after which deprecated API endpoints should be excluded.
* Use this to test your integration against upcoming deprecations.
*
* @param excludeDeprecatedAfter date string in YYYY-MM-DD format
*/
public void setExcludeDeprecatedAfter(Optional<String> excludeDeprecatedAfter) {
Utils.checkNotNull(excludeDeprecatedAfter, "excludeDeprecatedAfter");
this.excludeDeprecatedAfter = excludeDeprecatedAfter;
}

private Optional<Boolean> includeExperimental = Optional.empty();

/**
* Gets whether experimental API features should be enabled.
*
* @return Optional containing the boolean value if set
*/
public Optional<Boolean> includeExperimental() {
return includeExperimental;
}

/**
* Sets whether experimental API features should be enabled.
* When true, enables experimental API features that are not yet generally available.
*
* @param includeExperimental whether to include experimental features
*/
public void setIncludeExperimental(Optional<Boolean> includeExperimental) {
Utils.checkNotNull(includeExperimental, "includeExperimental");
this.includeExperimental = includeExperimental;
}
private ScheduledExecutorService retryScheduler = Executors.newSingleThreadScheduledExecutor();

public ScheduledExecutorService retryScheduler() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ private SDKHooks() {
public static void initialize(com.glean.api_client.glean_api_client.utils.Hooks hooks) {
hooks.registerAfterError(AgentFileUploadErrorHook.createSyncHook());

// Register the X-Glean header hook for experimental features and deprecation testing
hooks.registerBeforeRequest(XGleanHeadersHook.createSyncHook());

// for more information see
// https://www.speakeasy.com/docs/additional-features/sdk-hooks
}

public static void initialize(com.glean.api_client.glean_api_client.utils.AsyncHooks asyncHooks) {
asyncHooks.registerAfterError(AgentFileUploadErrorHook.createAsyncHook());

// Register the X-Glean header hook for experimental features and deprecation testing
asyncHooks.registerBeforeRequest(XGleanHeadersHook.createAsyncHook());

// NOTE: If you have existing synchronous hooks, you can adapt them using HookAdapters:
// asyncHooks.registerAfterError(com.glean.api_client.glean_api_client.utils.HookAdapters.adapt(mySyncHook));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package com.glean.api_client.glean_api_client.hooks;

import com.glean.api_client.glean_api_client.SDKConfiguration;
import com.glean.api_client.glean_api_client.utils.AsyncHook;
import com.glean.api_client.glean_api_client.utils.Helpers;
import com.glean.api_client.glean_api_client.utils.Hook;

import java.net.http.HttpRequest;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;

/**
* Hook that adds X-Glean headers for experimental features and deprecation testing.
*
* <p>This hook sets the following headers based on SDK options or environment variables:
* <ul>
* <li>{@code X-Glean-Exclude-Deprecated-After} - Exclude API endpoints deprecated after this date</li>
* <li>{@code X-Glean-Experimental} - Enable experimental API features</li>
* </ul>
*
* <p>Environment variables take precedence over SDK constructor options:
* <ul>
* <li>{@code X_GLEAN_EXCLUDE_DEPRECATED_AFTER} - Date in YYYY-MM-DD format</li>
* <li>{@code X_GLEAN_INCLUDE_EXPERIMENTAL} - "true" to enable experimental features</li>
* </ul>
*/
public final class XGleanHeadersHook {

static final String ENV_EXCLUDE_DEPRECATED_AFTER = "X_GLEAN_EXCLUDE_DEPRECATED_AFTER";
static final String ENV_INCLUDE_EXPERIMENTAL = "X_GLEAN_INCLUDE_EXPERIMENTAL";

static final String HEADER_EXCLUDE_DEPRECATED_AFTER = "X-Glean-Exclude-Deprecated-After";
static final String HEADER_EXPERIMENTAL = "X-Glean-Experimental";

private XGleanHeadersHook() {
// prevent instantiation
}

/**
* Creates a synchronous BeforeRequest hook for adding X-Glean headers.
*
* @return the sync hook
*/
public static Hook.BeforeRequest createSyncHook() {
return createSyncHook(System::getenv);
}

/**
* Creates a synchronous BeforeRequest hook for adding X-Glean headers.
* This variant accepts a custom environment variable provider for testing.
*
* @param envProvider function to get environment variables
* @return the sync hook
*/
static Hook.BeforeRequest createSyncHook(Function<String, String> envProvider) {
return (context, request) -> {
HttpRequest.Builder builder = Helpers.copy(request);
addHeaders(builder, context.sdkConfiguration(), envProvider);
return builder.build();
};
}

/**
* Creates an asynchronous BeforeRequest hook for adding X-Glean headers.
*
* @return the async hook
*/
public static AsyncHook.BeforeRequest createAsyncHook() {
return createAsyncHook(System::getenv);
}

/**
* Creates an asynchronous BeforeRequest hook for adding X-Glean headers.
* This variant accepts a custom environment variable provider for testing.
*
* @param envProvider function to get environment variables
* @return the async hook
*/
static AsyncHook.BeforeRequest createAsyncHook(Function<String, String> envProvider) {
return (context, request) -> {
HttpRequest.Builder builder = Helpers.copy(request);
addHeaders(builder, context.sdkConfiguration(), envProvider);
return CompletableFuture.completedFuture(builder.build());
};
}

private static void addHeaders(HttpRequest.Builder builder, SDKConfiguration config,
Function<String, String> envProvider) {
// Get deprecated after value - environment variable takes precedence
Optional<String> deprecatedAfterValue = getFirstNonEmpty(
getEnv(ENV_EXCLUDE_DEPRECATED_AFTER, envProvider),
config.excludeDeprecatedAfter()
);

deprecatedAfterValue.ifPresent(value ->
builder.header(HEADER_EXCLUDE_DEPRECATED_AFTER, value)
);

// Get experimental value - environment variable takes precedence
Optional<String> experimentalValue = getFirstNonEmpty(
getEnvAsBoolean(ENV_INCLUDE_EXPERIMENTAL, envProvider),
config.includeExperimental().filter(b -> b).map(b -> "true")
);

experimentalValue.ifPresent(value ->
builder.header(HEADER_EXPERIMENTAL, value)
);
}

/**
* Returns the first non-empty Optional from the provided arguments.
*/
@SafeVarargs
private static Optional<String> getFirstNonEmpty(Optional<String>... optionals) {
for (Optional<String> opt : optionals) {
if (opt.isPresent()) {
return opt;
}
}
return Optional.empty();
}

private static Optional<String> getEnv(String name, Function<String, String> envProvider) {
String value = envProvider.apply(name);
if (value != null && !value.isEmpty()) {
return Optional.of(value);
}
return Optional.empty();
}

private static Optional<String> getEnvAsBoolean(String name, Function<String, String> envProvider) {
String value = envProvider.apply(name);
if ("true".equalsIgnoreCase(value)) {
return Optional.of("true");
}
return Optional.empty();
}
}
Loading