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
32 changes: 31 additions & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,37 @@ typescript:
dependencies: {}
devDependencies: {}
peerDependencies: {}
additionalPackageJSON: {}
additionalPackageJSON:
exports:
./hooks:
import:
"@gleanwork/api-client/source": ./src/hooks/index.ts
types: ./dist/esm/hooks/index.d.ts
default: ./dist/esm/hooks/index.js
require:
types: ./dist/commonjs/hooks/index.d.ts
default: ./dist/commonjs/hooks/index.js
./hooks/*.js:
import:
"@gleanwork/api-client/source": ./src/hooks/*.ts
types: ./dist/esm/hooks/*.d.ts
default: ./dist/esm/hooks/*.js
require:
types: ./dist/commonjs/hooks/*.d.ts
default: ./dist/commonjs/hooks/*.js
./hooks/*:
import:
"@gleanwork/api-client/source": ./src/hooks/*.ts
types: ./dist/esm/hooks/*.d.ts
default: ./dist/esm/hooks/*.js
require:
types: ./dist/commonjs/hooks/*.d.ts
default: ./dist/commonjs/hooks/*.js
tshy:
exports:
./hooks: ./src/hooks/index.ts
./hooks/*.js: ./src/hooks/*.ts
./hooks/*: ./src/hooks/*.ts
additionalScripts: {}
alwaysIncludeInboundAndOutbound: false
author: Speakeasy
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1167,13 +1167,17 @@ const glean = new Glean({

```typescript
import { Glean } from "@gleanwork/api-client";
import type { SDKOptions } from "@gleanwork/api-client";
import type { XGleanOptions } from "@gleanwork/api-client/hooks/x-glean-options.js";

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

const glean = new Glean(opts);
```

### Option Reference
Expand Down
36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"./models/components": "./src/models/components/index.ts",
"./models/operations": "./src/models/operations/index.ts",
"./react-query": "./src/react-query/index.ts",
"./hooks": "./src/hooks/index.ts",
"./hooks/*.js": "./src/hooks/*.ts",
"./hooks/*": "./src/hooks/*.ts",
"./*.js": "./src/*.ts",
"./*": "./src/*.ts"
}
Expand Down Expand Up @@ -130,6 +133,39 @@
"default": "./dist/commonjs/react-query/index.js"
}
},
"./hooks": {
"import": {
"@gleanwork/api-client/source": "./src/hooks/index.ts",
"types": "./dist/esm/hooks/index.d.ts",
"default": "./dist/esm/hooks/index.js"
},
"require": {
"types": "./dist/commonjs/hooks/index.d.ts",
"default": "./dist/commonjs/hooks/index.js"
}
},
"./hooks/*.js": {
"import": {
"@gleanwork/api-client/source": "./src/hooks/*.ts",
"types": "./dist/esm/hooks/*.d.ts",
"default": "./dist/esm/hooks/*.js"
},
"require": {
"types": "./dist/commonjs/hooks/*.d.ts",
"default": "./dist/commonjs/hooks/*.js"
}
},
"./hooks/*": {
"import": {
"@gleanwork/api-client/source": "./src/hooks/*.ts",
"types": "./dist/esm/hooks/*.d.ts",
"default": "./dist/esm/hooks/*.js"
},
"require": {
"types": "./dist/commonjs/hooks/*.d.ts",
"default": "./dist/commonjs/hooks/*.js"
}
},
"./*.js": {
"import": {
"@gleanwork/api-client/source": "./src/*.ts",
Expand Down
56 changes: 56 additions & 0 deletions src/__tests__/x-glean.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,34 @@ describe("XGlean hook", () => {
);
expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});

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

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

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

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

it("should fall back to SDK options when experimental env var is empty", () => {
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = " ";

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");
});
});

describe("environment variables take precedence over SDK options", () => {
Expand Down Expand Up @@ -202,5 +230,33 @@ describe("XGlean hook", () => {
);
expect(result.headers.get("X-Glean-Experimental")).toBe("true");
});

it("should omit X-Glean-Experimental header when env var is false, even if option is true", () => {
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"] = "false";

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

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

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

describe("when options are malformed at runtime", () => {
it("should not set X-Glean-Exclude-Deprecated-After when excludeDeprecatedAfter is not a string", () => {
const hook = new XGlean();
const request = createMockRequest();
const context = createMockContext({
excludeDeprecatedAfter: 123 as any,
});

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

expect(result.headers.has("X-Glean-Exclude-Deprecated-After")).toBe(false);
});
});
});
4 changes: 2 additions & 2 deletions src/hooks/x-glean-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ export interface XGleanOptions {
*
* More information: https://developers.glean.com/deprecations/overview
*/
excludeDeprecatedAfter?: string | undefined;
excludeDeprecatedAfter?: string;
/**
* When true, enables experimental API features that are not yet generally available.
* Use this to preview and test new functionality.
*/
includeExperimental?: boolean | undefined;
includeExperimental?: boolean;
}
52 changes: 41 additions & 11 deletions src/hooks/x-glean.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BeforeRequestContext, BeforeRequestHook } from "./types.js";
import { XGleanOptions } from "./x-glean-options.js";

/**
* Get the first non-empty value from the provided arguments.
Expand All @@ -8,26 +7,57 @@ function getFirstValue(
aValue: string | undefined,
bValue: string | undefined,
): string | false {
if (aValue) return aValue;
if (bValue) return bValue;
if (aValue && aValue.trim()) return aValue.trim();
if (bValue && bValue.trim()) return bValue.trim();
return false;
}

function getStringOption(
options: Record<string, unknown>,
key: string,
): string | undefined {
const v = options[key];
return typeof v === "string" && v.trim() ? v.trim() : undefined;
}

function getBooleanOption(options: Record<string, unknown>, key: string): boolean {
return options[key] === true;
}

export class XGlean implements BeforeRequestHook {
beforeRequest(hookCtx: BeforeRequestContext, request: Request): Request {
// Cast options to include X-Glean custom properties
// These properties may be passed by users but aren't in the generated SDKOptions type
const options = hookCtx.options as XGleanOptions;
const opt = hookCtx.options as unknown;
const options = typeof opt === "object" && opt != null
? (opt as Record<string, unknown>)
: {};

const deprecatedValue = getFirstValue(
process.env["X_GLEAN_EXCLUDE_DEPRECATED_AFTER"],
options.excludeDeprecatedAfter,
getStringOption(options, "excludeDeprecatedAfter"),
);

const experimentalValue = getFirstValue(
process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"],
options.includeExperimental === true ? "true" : undefined,
);
const experimentalEnv = process.env["X_GLEAN_INCLUDE_EXPERIMENTAL"];
let experimentalValue: string | false = false;
if (typeof experimentalEnv !== "undefined") {
const t = experimentalEnv.trim();
// Env var is treated as a boolean toggle.
// - "true" (case-insensitive) => set header
// - anything else (including "false") => omit header
// - empty/whitespace => treat as unset and fall back to options
if (!t) {
experimentalValue = getBooleanOption(options, "includeExperimental")
? "true"
: false;
} else if (t.toLowerCase() === "true") {
experimentalValue = "true";
} else {
experimentalValue = false;
}
} else {
experimentalValue = getBooleanOption(options, "includeExperimental")
? "true"
: false;
}

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