From 10766b8763f0a92e50ad23bbbf53d41dd2a332b6 Mon Sep 17 00:00:00 2001 From: Travis Hoover Date: Thu, 22 Jan 2026 14:42:54 -0800 Subject: [PATCH 1/3] feat: add X-Glean headers for experimental features and deprecation testing Add a new beforeRequest hook that sets X-Glean-Exclude-Deprecated-After and X-Glean-Experimental headers based on SDK options or environment variables. This allows users to test upcoming API changes and preview experimental features before they become the default behavior. --- README.md | 53 +++++++++++++++++++++++++++++++++++++++ src/hooks/registration.ts | 6 ++++- src/hooks/x-glean.ts | 37 +++++++++++++++++++++++++++ src/lib/config.ts | 11 ++++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/hooks/x-glean.ts diff --git a/README.md b/README.md index 6faf4e87..d75752ef 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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. +## 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. + # Development diff --git a/src/hooks/registration.ts b/src/hooks/registration.ts index 45470b5d..2b00fb65 100644 --- a/src/hooks/registration.ts +++ b/src/hooks/registration.ts @@ -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. @@ -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()); } diff --git a/src/hooks/x-glean.ts b/src/hooks/x-glean.ts new file mode 100644 index 00000000..ab7d2c3d --- /dev/null +++ b/src/hooks/x-glean.ts @@ -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; + } +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 20bc3868..c2fe0953 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -40,6 +40,17 @@ 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') + */ + 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; }; export function serverURLFromOptions(options: SDKOptions): URL | null { From da20234dca967bc094de3aaa7d64c94fec7fec26 Mon Sep 17 00:00:00 2001 From: Travis Hoover Date: Thu, 22 Jan 2026 15:00:26 -0800 Subject: [PATCH 2/3] refactor: rename X-Glean env vars to use uppercase convention Rename environment variables to follow standard uppercase naming: - X_Glean_Exclude_Deprecated_After -> X_GLEAN_EXCLUDE_DEPRECATED_AFTER - X_Glean_Include_Experimental -> X_GLEAN_INCLUDE_EXPERIMENTAL --- README.md | 8 ++++---- src/hooks/x-glean.ts | 4 ++-- src/lib/config.ts | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d75752ef..0fc35f4e 100644 --- a/README.md +++ b/README.md @@ -1152,8 +1152,8 @@ You can configure these options either via environment variables or SDK construc ```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'; +process.env.X_GLEAN_EXCLUDE_DEPRECATED_AFTER = '2026-10-15'; +process.env.X_GLEAN_INCLUDE_EXPERIMENTAL = 'true'; import { Glean } from "@gleanwork/api-client"; @@ -1180,8 +1180,8 @@ const glean = new Glean({ | 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. | +| `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. diff --git a/src/hooks/x-glean.ts b/src/hooks/x-glean.ts index ab7d2c3d..491ecf84 100644 --- a/src/hooks/x-glean.ts +++ b/src/hooks/x-glean.ts @@ -15,12 +15,12 @@ function getFirstValue( export class XGlean implements BeforeRequestHook { beforeRequest(hookCtx: BeforeRequestContext, request: Request): Request { const deprecatedValue = getFirstValue( - process.env["X_Glean_Exclude_Deprecated_After"], + process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"], hookCtx.options.excludeDeprecatedAfter, ); const experimentalValue = getFirstValue( - process.env["X_Glean_Include_Experimental"], + process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"], hookCtx.options.includeExperimental === true ? "true" : undefined, ); diff --git a/src/lib/config.ts b/src/lib/config.ts index c2fe0953..259cb401 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -44,6 +44,8 @@ export type SDKOptions = { * 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; /** From 9f0235e45f0af5e7fddef204c43012e529cd064c Mon Sep 17 00:00:00 2001 From: Travis Hoover Date: Thu, 22 Jan 2026 15:04:02 -0800 Subject: [PATCH 3/3] test: add unit tests for XGlean hook Add comprehensive tests covering: - Headers set correctly via SDK constructor options - Headers set correctly via environment variables - Environment variables take precedence over SDK options - No headers set when neither option is configured --- src/__tests__/x-glean.test.ts | 199 ++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 src/__tests__/x-glean.test.ts diff --git a/src/__tests__/x-glean.test.ts b/src/__tests__/x-glean.test.ts new file mode 100644 index 00000000..342e1c6a --- /dev/null +++ b/src/__tests__/x-glean.test.ts @@ -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"); + }); + }); +});