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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ These API clients provide type-safe, idiomatic interfaces for working with Glean
* [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 @@ -1136,6 +1137,58 @@ const sdk = new Glean({ debugLogger: console });
You can also enable a default debug logger by setting an environment variable `GLEAN_DEBUG` to true.
<!-- 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

```typescript
// Set environment variables before initializing the SDK
process.env.X_GLEAN_EXCLUDE_DEPRECATED_AFTER = '2026-10-15';
process.env.X_GLEAN_INCLUDE_EXPERIMENTAL = 'true';

import { Glean } from "@gleanwork/api-client";

const glean = new Glean({
apiToken: process.env["GLEAN_API_TOKEN"] ?? "",
instance: process.env["GLEAN_INSTANCE"] ?? "",
});
```

#### Using SDK Constructor Options

```typescript
import { Glean } from "@gleanwork/api-client";

const glean = new Glean({
apiToken: process.env["GLEAN_API_TOKEN"] ?? "",
instance: process.env["GLEAN_INSTANCE"] ?? "",
excludeDeprecatedAfter: '2026-10-15',
includeExperimental: true,
});
```

### 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
199 changes: 199 additions & 0 deletions src/__tests__/x-glean.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { XGlean } from "../hooks/x-glean.js";
import { BeforeRequestContext } from "../hooks/types.js";
import { SDKOptions } from "../lib/config.js";

function createMockRequest(): Request {
return new Request("https://example.com/api/test");
}

function createMockContext(options: SDKOptions = {}): BeforeRequestContext {
return {
baseURL: "https://example.com",
operationID: "test-operation",
oAuth2Scopes: null,
retryConfig: { strategy: "none" },
resolvedSecurity: null,
options,
};
}

describe("XGlean hook", () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = { ...originalEnv };
delete process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"];
delete process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"];
});

afterEach(() => {
process.env = originalEnv;
});

describe("when neither options nor environment variables are set", () => {
it("should not set any X-Glean headers", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({});

const result = hook.beforeRequest(context, request);

expect(result.headers.has("X-Glean-Exclude-Deprecated-After")).toBe(
false,
);
expect(result.headers.has("X-Glean-Experimental")).toBe(false);
});
});

describe("when using SDK constructor options", () => {
it("should set X-Glean-Exclude-Deprecated-After header from excludeDeprecatedAfter option", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
excludeDeprecatedAfter: "2026-10-15",
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2026-10-15",
);
});

it("should set X-Glean-Experimental header when includeExperimental is true", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
includeExperimental: true,
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});

it("should not set X-Glean-Experimental header when includeExperimental is false", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
includeExperimental: false,
});

const result = hook.beforeRequest(context, request);

expect(result.headers.has("X-Glean-Experimental")).toBe(false);
});

it("should set both headers when both options are provided", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
excludeDeprecatedAfter: "2026-10-15",
includeExperimental: true,
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2026-10-15",
);
expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});
});

describe("when using environment variables", () => {
it("should set X-Glean-Exclude-Deprecated-After header from environment variable", () => {
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"] = "2027-01-01";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2027-01-01",
);
});

it("should set X-Glean-Experimental header from environment variable", () => {
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = "true";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});

it("should set both headers from environment variables", () => {
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"] = "2027-06-15";
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = "true";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2027-06-15",
);
expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});
});

describe("environment variables take precedence over SDK options", () => {
it("should use environment variable for excludeDeprecatedAfter when both are set", () => {
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"] = "2027-12-31";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
excludeDeprecatedAfter: "2026-01-01",
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2027-12-31",
);
});

it("should use environment variable for includeExperimental when both are set", () => {
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = "true";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
includeExperimental: false,
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});

it("should use environment variables for both headers when all are set", () => {
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"] = "2028-01-01";
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = "true";

const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
excludeDeprecatedAfter: "2026-06-01",
includeExperimental: false,
});

const result = hook.beforeRequest(context, request);

expect(result.headers.get("X-Glean-Exclude-Deprecated-After")).toBe(
"2028-01-01",
);
expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});
});
});
6 changes: 5 additions & 1 deletion src/hooks/registration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hooks, AfterErrorHook } from './types.js';
import { Hooks, AfterErrorHook } from "./types.js";
import { XGlean } from "./x-glean.js";

/*
* This file is only ever generated once on the first generation and then is free to be modified.
Expand Down Expand Up @@ -47,4 +48,7 @@ export function initHooks(hooks: Hooks) {
// with an instance of a hook that implements that specific Hook interface
// Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance
hooks.registerAfterErrorHook(agentFileUploadErrorHook);

// Register the X-Glean header hook for experimental features and deprecation testing
hooks.registerBeforeRequestHook(new XGlean());
}
37 changes: 37 additions & 0 deletions src/hooks/x-glean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BeforeRequestContext, BeforeRequestHook } from "./types.js";

/**
* Get the first non-empty value from the provided arguments.
*/
function getFirstValue(
aValue: string | undefined,
bValue: string | undefined,
): string | false {
if (aValue) return aValue;
if (bValue) return bValue;
return false;
}

export class XGlean implements BeforeRequestHook {
beforeRequest(hookCtx: BeforeRequestContext, request: Request): Request {
const deprecatedValue = getFirstValue(
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"],
hookCtx.options.excludeDeprecatedAfter,
);

const experimentalValue = getFirstValue(
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"],
hookCtx.options.includeExperimental === true ? "true" : undefined,
);

if (deprecatedValue) {
request.headers.set("X-Glean-Exclude-Deprecated-After", deprecatedValue);
}

if (experimentalValue) {
request.headers.set("X-Glean-Experimental", experimentalValue);
}

return request;
}
}
13 changes: 13 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ export type SDKOptions = {
retryConfig?: RetryConfig;
timeoutMs?: number;
debugLogger?: Logger;
/**
* Exclude API endpoints that will be deprecated after this date.
* Use this to test your integration against upcoming deprecations.
* Format: YYYY-MM-DD (e.g., '2026-10-15')
*
* More information: https://developers.glean.com/deprecations/overview
*/
excludeDeprecatedAfter?: string | undefined;
/**
* When true, enables experimental API features that are not yet generally available.
* Use this to preview and test new functionality.
*/
includeExperimental?: boolean | undefined;
Comment on lines +51 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3/5 (strong opinion: non-blocking)

Same thing here, maybe a link to the website would be great.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now we dont have anything on the experimental stuff (not sure if we want to list out experiments or not) but if we do then I will add a link to that here

};

export function serverURLFromOptions(options: SDKOptions): URL | null {
Expand Down