Skip to content

Commit 5ba9a83

Browse files
Merge pull request #66 from gleanwork/feat/x-glean-headers
feat: add X-Glean headers for experimental features and deprecation testing
2 parents 1a11072 + 763421f commit 5ba9a83

File tree

7 files changed

+531
-2
lines changed

7 files changed

+531
-2
lines changed

README.md

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

1602+
## Experimental Features and Deprecation Testing
1603+
1604+
The SDK provides options to test upcoming API changes before they become the default behavior. This is useful for:
1605+
1606+
- **Testing experimental features** before they are generally available
1607+
- **Preparing for deprecations** by excluding deprecated endpoints ahead of their removal
1608+
1609+
### Configuration Options
1610+
1611+
You can configure these options either via environment variables or SDK constructor options:
1612+
1613+
#### Using Environment Variables
1614+
1615+
```bash
1616+
# Set environment variables before running your application
1617+
export X_GLEAN_EXCLUDE_DEPRECATED_AFTER="2026-10-15"
1618+
export X_GLEAN_INCLUDE_EXPERIMENTAL="true"
1619+
```
1620+
1621+
```java
1622+
// Environment variables are automatically read by the SDK
1623+
Glean glean = Glean.builder()
1624+
.apiToken(System.getenv("GLEAN_API_TOKEN"))
1625+
.instance("instance-name")
1626+
.build();
1627+
```
1628+
1629+
#### Using SDK Constructor Options
1630+
1631+
```java
1632+
Glean glean = Glean.builder()
1633+
.apiToken(System.getenv("GLEAN_API_TOKEN"))
1634+
.instance("instance-name")
1635+
.excludeDeprecatedAfter("2026-10-15")
1636+
.includeExperimental(true)
1637+
.build();
1638+
```
1639+
1640+
### Option Reference
1641+
1642+
| Option | Environment Variable | Type | Description |
1643+
| ------ | -------------------- | ---- | ----------- |
1644+
| `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. |
1645+
| `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. |
1646+
1647+
> **Note:** Environment variables take precedence over SDK constructor options when both are set.
1648+
1649+
> **Warning:** Experimental features may change or be removed without notice. Do not rely on experimental features in production environments.
1650+
16011651
<!-- Placeholder for Future Speakeasy SDK Sections -->
16021652

16031653
# Development

build-extras.gradle

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
// This file
1+
// This file
22
// * is referred to in an `apply from` command in `build.gradle`
33
// * can be used to customise `build.gradle`
44
// * is generated once and not overwritten in SDK generation updates
5+
6+
dependencies {
7+
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
8+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
9+
}
10+
11+
test {
12+
useJUnitPlatform()
13+
}

src/main/java/com/glean/api_client/glean_api_client/Glean.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,35 @@ public Builder instance(String instance) {
204204

205205
return this;
206206
}
207+
208+
/**
209+
* Exclude API endpoints that will be deprecated after this date.
210+
* Use this to test your integration against upcoming deprecations.
211+
*
212+
* <p>More information: <a href="https://developers.glean.com/deprecations/overview">Deprecations Overview</a>
213+
*
214+
* @param excludeDeprecatedAfter date string in YYYY-MM-DD format (e.g., '2026-10-15')
215+
* @return The builder instance.
216+
*/
217+
public Builder excludeDeprecatedAfter(String excludeDeprecatedAfter) {
218+
this.sdkConfiguration.setExcludeDeprecatedAfter(Optional.of(excludeDeprecatedAfter));
219+
return this;
220+
}
221+
222+
/**
223+
* Enable experimental API features that are not yet generally available.
224+
* Use this to preview and test new functionality.
225+
*
226+
* <p><strong>Warning:</strong> Experimental features may change or be removed without notice.
227+
* Do not rely on experimental features in production environments.
228+
*
229+
* @param includeExperimental whether to include experimental features
230+
* @return The builder instance.
231+
*/
232+
public Builder includeExperimental(boolean includeExperimental) {
233+
this.sdkConfiguration.setIncludeExperimental(Optional.of(includeExperimental));
234+
return this;
235+
}
207236
// Visible for testing, may be accessed via reflection in tests
208237
Builder _hooks(com.glean.api_client.glean_api_client.utils.Hooks hooks) {
209238
sdkConfiguration.setHooks(hooks);

src/main/java/com/glean/api_client/glean_api_client/SDKConfiguration.java

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public Map<String, String> getServerVariableDefaults() {
128128
return serverVariables.get(this.serverIdx);
129129
}
130130
private Optional<RetryConfig> retryConfig = Optional.empty();
131-
131+
132132
public Optional<RetryConfig> retryConfig() {
133133
return retryConfig;
134134
}
@@ -137,6 +137,50 @@ public void setRetryConfig(Optional<RetryConfig> retryConfig) {
137137
Utils.checkNotNull(retryConfig, "retryConfig");
138138
this.retryConfig = retryConfig;
139139
}
140+
141+
private Optional<String> excludeDeprecatedAfter = Optional.empty();
142+
143+
/**
144+
* Gets the date after which deprecated API endpoints should be excluded.
145+
*
146+
* @return Optional containing the date string (YYYY-MM-DD format) if set
147+
*/
148+
public Optional<String> excludeDeprecatedAfter() {
149+
return excludeDeprecatedAfter;
150+
}
151+
152+
/**
153+
* Sets the date after which deprecated API endpoints should be excluded.
154+
* Use this to test your integration against upcoming deprecations.
155+
*
156+
* @param excludeDeprecatedAfter date string in YYYY-MM-DD format
157+
*/
158+
public void setExcludeDeprecatedAfter(Optional<String> excludeDeprecatedAfter) {
159+
Utils.checkNotNull(excludeDeprecatedAfter, "excludeDeprecatedAfter");
160+
this.excludeDeprecatedAfter = excludeDeprecatedAfter;
161+
}
162+
163+
private Optional<Boolean> includeExperimental = Optional.empty();
164+
165+
/**
166+
* Gets whether experimental API features should be enabled.
167+
*
168+
* @return Optional containing the boolean value if set
169+
*/
170+
public Optional<Boolean> includeExperimental() {
171+
return includeExperimental;
172+
}
173+
174+
/**
175+
* Sets whether experimental API features should be enabled.
176+
* When true, enables experimental API features that are not yet generally available.
177+
*
178+
* @param includeExperimental whether to include experimental features
179+
*/
180+
public void setIncludeExperimental(Optional<Boolean> includeExperimental) {
181+
Utils.checkNotNull(includeExperimental, "includeExperimental");
182+
this.includeExperimental = includeExperimental;
183+
}
140184
private ScheduledExecutorService retryScheduler = Executors.newSingleThreadScheduledExecutor();
141185

142186
public ScheduledExecutorService retryScheduler() {

src/main/java/com/glean/api_client/glean_api_client/hooks/SDKHooks.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ private SDKHooks() {
1515
public static void initialize(com.glean.api_client.glean_api_client.utils.Hooks hooks) {
1616
hooks.registerAfterError(AgentFileUploadErrorHook.createSyncHook());
1717

18+
// Register the X-Glean header hook for experimental features and deprecation testing
19+
hooks.registerBeforeRequest(XGleanHeadersHook.createSyncHook());
20+
1821
// for more information see
1922
// https://www.speakeasy.com/docs/additional-features/sdk-hooks
2023
}
2124

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

28+
// Register the X-Glean header hook for experimental features and deprecation testing
29+
asyncHooks.registerBeforeRequest(XGleanHeadersHook.createAsyncHook());
30+
2531
// NOTE: If you have existing synchronous hooks, you can adapt them using HookAdapters:
2632
// asyncHooks.registerAfterError(com.glean.api_client.glean_api_client.utils.HookAdapters.adapt(mySyncHook));
2733

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package com.glean.api_client.glean_api_client.hooks;
2+
3+
import com.glean.api_client.glean_api_client.SDKConfiguration;
4+
import com.glean.api_client.glean_api_client.utils.AsyncHook;
5+
import com.glean.api_client.glean_api_client.utils.Helpers;
6+
import com.glean.api_client.glean_api_client.utils.Hook;
7+
8+
import java.net.http.HttpRequest;
9+
import java.util.Optional;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.function.Function;
12+
13+
/**
14+
* Hook that adds X-Glean headers for experimental features and deprecation testing.
15+
*
16+
* <p>This hook sets the following headers based on SDK options or environment variables:
17+
* <ul>
18+
* <li>{@code X-Glean-Exclude-Deprecated-After} - Exclude API endpoints deprecated after this date</li>
19+
* <li>{@code X-Glean-Experimental} - Enable experimental API features</li>
20+
* </ul>
21+
*
22+
* <p>Environment variables take precedence over SDK constructor options:
23+
* <ul>
24+
* <li>{@code X_GLEAN_EXCLUDE_DEPRECATED_AFTER} - Date in YYYY-MM-DD format</li>
25+
* <li>{@code X_GLEAN_INCLUDE_EXPERIMENTAL} - "true" to enable experimental features</li>
26+
* </ul>
27+
*/
28+
public final class XGleanHeadersHook {
29+
30+
static final String ENV_EXCLUDE_DEPRECATED_AFTER = "X_GLEAN_EXCLUDE_DEPRECATED_AFTER";
31+
static final String ENV_INCLUDE_EXPERIMENTAL = "X_GLEAN_INCLUDE_EXPERIMENTAL";
32+
33+
static final String HEADER_EXCLUDE_DEPRECATED_AFTER = "X-Glean-Exclude-Deprecated-After";
34+
static final String HEADER_EXPERIMENTAL = "X-Glean-Experimental";
35+
36+
private XGleanHeadersHook() {
37+
// prevent instantiation
38+
}
39+
40+
/**
41+
* Creates a synchronous BeforeRequest hook for adding X-Glean headers.
42+
*
43+
* @return the sync hook
44+
*/
45+
public static Hook.BeforeRequest createSyncHook() {
46+
return createSyncHook(System::getenv);
47+
}
48+
49+
/**
50+
* Creates a synchronous BeforeRequest hook for adding X-Glean headers.
51+
* This variant accepts a custom environment variable provider for testing.
52+
*
53+
* @param envProvider function to get environment variables
54+
* @return the sync hook
55+
*/
56+
static Hook.BeforeRequest createSyncHook(Function<String, String> envProvider) {
57+
return (context, request) -> {
58+
HttpRequest.Builder builder = Helpers.copy(request);
59+
addHeaders(builder, context.sdkConfiguration(), envProvider);
60+
return builder.build();
61+
};
62+
}
63+
64+
/**
65+
* Creates an asynchronous BeforeRequest hook for adding X-Glean headers.
66+
*
67+
* @return the async hook
68+
*/
69+
public static AsyncHook.BeforeRequest createAsyncHook() {
70+
return createAsyncHook(System::getenv);
71+
}
72+
73+
/**
74+
* Creates an asynchronous BeforeRequest hook for adding X-Glean headers.
75+
* This variant accepts a custom environment variable provider for testing.
76+
*
77+
* @param envProvider function to get environment variables
78+
* @return the async hook
79+
*/
80+
static AsyncHook.BeforeRequest createAsyncHook(Function<String, String> envProvider) {
81+
return (context, request) -> {
82+
HttpRequest.Builder builder = Helpers.copy(request);
83+
addHeaders(builder, context.sdkConfiguration(), envProvider);
84+
return CompletableFuture.completedFuture(builder.build());
85+
};
86+
}
87+
88+
private static void addHeaders(HttpRequest.Builder builder, SDKConfiguration config,
89+
Function<String, String> envProvider) {
90+
// Get deprecated after value - environment variable takes precedence
91+
Optional<String> deprecatedAfterValue = getFirstNonEmpty(
92+
getEnv(ENV_EXCLUDE_DEPRECATED_AFTER, envProvider),
93+
config.excludeDeprecatedAfter()
94+
);
95+
96+
deprecatedAfterValue.ifPresent(value ->
97+
builder.header(HEADER_EXCLUDE_DEPRECATED_AFTER, value)
98+
);
99+
100+
// Get experimental value - environment variable takes precedence
101+
Optional<String> experimentalValue = getFirstNonEmpty(
102+
getEnvAsBoolean(ENV_INCLUDE_EXPERIMENTAL, envProvider),
103+
config.includeExperimental().filter(b -> b).map(b -> "true")
104+
);
105+
106+
experimentalValue.ifPresent(value ->
107+
builder.header(HEADER_EXPERIMENTAL, value)
108+
);
109+
}
110+
111+
/**
112+
* Returns the first non-empty Optional from the provided arguments.
113+
*/
114+
@SafeVarargs
115+
private static Optional<String> getFirstNonEmpty(Optional<String>... optionals) {
116+
for (Optional<String> opt : optionals) {
117+
if (opt.isPresent()) {
118+
return opt;
119+
}
120+
}
121+
return Optional.empty();
122+
}
123+
124+
private static Optional<String> getEnv(String name, Function<String, String> envProvider) {
125+
String value = envProvider.apply(name);
126+
if (value != null && !value.isEmpty()) {
127+
return Optional.of(value);
128+
}
129+
return Optional.empty();
130+
}
131+
132+
private static Optional<String> getEnvAsBoolean(String name, Function<String, String> envProvider) {
133+
String value = envProvider.apply(name);
134+
if ("true".equalsIgnoreCase(value)) {
135+
return Optional.of("true");
136+
}
137+
return Optional.empty();
138+
}
139+
}

0 commit comments

Comments
 (0)