Skip to content
Draft
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
12 changes: 12 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ The build is orchestrated by Turbo with these key dependencies:

**Authentication: The primary auth mechanism for the remote MCP server is OAuth. Do not tell users they need an API token — that is one option but not the default or recommended path.**

## Before Pushing a PR

Always run these checks before committing or pushing, in this order:

```bash
pnpm format # Auto-fix all formatting issues
pnpm prettier --check . # Verify nothing was missed
pnpm test # Confirm tests pass
```

Formatting failures will cause CI to fail immediately. Fix them locally first.

## Content Guidelines

- Prefer built-in Docusaurus components over custom React components
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/mcp/RemoteMCPContent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
import { CLIENT, MCPConfigRegistry } from '@gleanwork/mcp-config-schema/browser';
import { clientNeedsMcpRemote } from '@gleanwork/mcp-config-schema';
import { FeatureFlagsContext } from '@site/src/theme/Root';



# <Icon name="mcp" iconSet="glean" className="inline" height="1.4rem" /> Model Context Protocol (MCP) Remote Server
Expand Down
1 change: 0 additions & 1 deletion docs/guides/mcp/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ hide_table_of_contents: false
---

import React from 'react';
import FeatureFlag from '@site/src/components/FeatureFlag';
import RemoteMCPContent from './RemoteMCPContent.mdx';

<RemoteMCPContent />
28 changes: 8 additions & 20 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const redirects = [
...require('./redirects.json'),
...require('./permalinks.json'),
];
import { getBuildTimeFlags } from './src/utils/buildTimeFlags';
import { flagsSnapshotToBooleans } from './src/lib/featureFlags';
import { withFeatureFlags } from '@gleanwork/docusaurus-plugin-feature-flags/withFeatureFlags';

// Optional environment variable for Google site verification
const googleSiteVerification = process.env.GOOGLE_SITE_VERIFICATION;
Expand Down Expand Up @@ -71,14 +70,7 @@ const config: Config = {
src: 'img/glean-developer-logo-light.svg',
srcDark: 'img/glean-developer-logo-dark.svg',
},
items: ((items) => {
const { getBuildTimeFlags } = require('./src/utils/buildTimeFlags');
const { flagsSnapshotToBooleans } = require('./src/lib/featureFlags');
const { getNavbarItems } = require('./src/utils/filtering');
const raw = getBuildTimeFlags();
const bools = flagsSnapshotToBooleans(raw, {});
return getNavbarItems(items, bools);
})([
items: [
{
type: 'custom-mcpInstallButton',
position: 'right',
Expand Down Expand Up @@ -124,7 +116,7 @@ const config: Config = {
},
],
},
]),
],
},
prism: {
theme: prismThemes.github,
Expand Down Expand Up @@ -183,6 +175,10 @@ const config: Config = {
} satisfies Preset.ThemeConfig,

plugins: [
[
'@gleanwork/docusaurus-plugin-feature-flags',
{ apiEndpoint: '/api/feature-flags' },
],
function (context, options) {
return {
name: 'webpack-config',
Expand Down Expand Up @@ -288,14 +284,6 @@ const config: Config = {
onBrokenMarkdownLinks: 'throw',
},
},
customFields: (() => {
const raw = getBuildTimeFlags();
const booleans = flagsSnapshotToBooleans(raw, {});
return {
__BUILD_FLAGS__: raw,
__BUILD_FLAGS_BOOLEANS__: booleans,
} as Record<string, unknown>;
})(),
};

export default config;
export default withFeatureFlags(config);
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"@docusaurus/utils": "^3.9.2",
"@docusaurus/utils-validation": "^3.9.2",
"@gleanwork/api-client": "^0.13.4",
"@gleanwork/docusaurus-plugin-feature-flags": "workspace:*",
"@gleanwork/docusaurus-theme-glean": "workspace:*",
"@gleanwork/mcp-config-schema": "^4.1.0",
"@intercom/messenger-js-sdk": "^0.0.18",
Expand Down
21 changes: 21 additions & 0 deletions packages/docusaurus-plugin-feature-flags/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@gleanwork/docusaurus-plugin-feature-flags",
"version": "0.1.0",
"private": true,
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./context": "./src/theme/FeatureFlagsProvider/context.ts",
"./types": "./src/types.ts",
"./evaluation": "./src/lib/featureFlags.ts",
"./build": "./src/build/buildTimeFlags.ts",
"./filtering": "./src/build/filtering.ts",
"./withFeatureFlags": "./src/withFeatureFlags.ts",
"./FeatureFlag": "./src/theme/FeatureFlag/index.tsx"
},
"peerDependencies": {
"@docusaurus/core": "^3.0.0",
"@docusaurus/plugin-content-docs": "^3.0.0",
"react": "^18.0.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export function getBuildTimeFlags(): FeatureFlagsMap {

for (const [key, value] of Object.entries(process.env)) {
if (!key.startsWith('FF_')) continue;
// converts FF_FOO_BAZ_BAR to foo-baz-bar
const slug = key.replace(/^FF_/, '').toLowerCase().replace(/_/g, '-');
flags[slug] = { enabled: value === 'true' };
}
Expand Down
File renamed without changes.
22 changes: 22 additions & 0 deletions packages/docusaurus-plugin-feature-flags/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path';
import type { Plugin } from '@docusaurus/types';
import type { FeatureFlagPluginOptions } from './types';

export default function pluginFeatureFlags(
context: any,
options: FeatureFlagPluginOptions,
): Plugin<void> {
return {
name: 'docusaurus-plugin-feature-flags',
getThemePath() {
return path.join(__dirname, 'theme');
},
async contentLoaded({ actions }) {
actions.setGlobalData({
apiEndpoint: options.apiEndpoint ?? '/api/feature-flags',
cacheTtlMs: options.cacheTtlMs ?? 300_000,
debug: options.debug ?? false,
});
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,3 @@ export type FeatureEvaluationResult = {
enabled: boolean;
reason: 'explicit' | 'allowed-user' | 'rollout' | 'disabled' | 'missing' | 'not-yet-enabled' | 'expired';
};



Original file line number Diff line number Diff line change
Expand Up @@ -11,63 +11,63 @@ describe('evaluateFlag', () => {
enableAfter: '2025-01-01T00:00:00Z',
},
};

const result = evaluateFlag(flags, 'future-feature', {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result.enabled).toBe(false);
expect(result.reason).toBe('not-yet-enabled');
});

it('should enable flag after enableAfter date', () => {
const flags: FeatureFlagsMap = {
'future-feature': {
enabled: true,
enableAfter: '2024-01-01T00:00:00Z',
},
};

const result = evaluateFlag(flags, 'future-feature', {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result.enabled).toBe(true);
expect(result.reason).toBe('explicit');
});

it('should disable flag after disableAfter date', () => {
const flags: FeatureFlagsMap = {
'expired-feature': {
enabled: true,
disableAfter: '2024-01-01T00:00:00Z',
},
};

const result = evaluateFlag(flags, 'expired-feature', {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result.enabled).toBe(false);
expect(result.reason).toBe('expired');
});

it('should enable flag before disableAfter date', () => {
const flags: FeatureFlagsMap = {
'active-feature': {
enabled: true,
disableAfter: '2025-01-01T00:00:00Z',
},
};

const result = evaluateFlag(flags, 'active-feature', {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result.enabled).toBe(true);
expect(result.reason).toBe('explicit');
});

it('should handle both enableAfter and disableAfter', () => {
const flags: FeatureFlagsMap = {
'time-window-feature': {
Expand All @@ -76,26 +76,26 @@ describe('evaluateFlag', () => {
disableAfter: '2024-12-31T23:59:59Z',
},
};

const beforeWindow = evaluateFlag(flags, 'time-window-feature', {
currentTime: '2024-05-01T00:00:00Z',
});
expect(beforeWindow.enabled).toBe(false);
expect(beforeWindow.reason).toBe('not-yet-enabled');

const duringWindow = evaluateFlag(flags, 'time-window-feature', {
currentTime: '2024-09-01T00:00:00Z',
});
expect(duringWindow.enabled).toBe(true);
expect(duringWindow.reason).toBe('explicit');

const afterWindow = evaluateFlag(flags, 'time-window-feature', {
currentTime: '2025-01-01T00:00:00Z',
});
expect(afterWindow.enabled).toBe(false);
expect(afterWindow.reason).toBe('expired');
});

it('should handle invalid date strings gracefully', () => {
const flags: FeatureFlagsMap = {
'invalid-dates': {
Expand All @@ -104,15 +104,15 @@ describe('evaluateFlag', () => {
disableAfter: 'also-not-a-date',
},
};

const result = evaluateFlag(flags, 'invalid-dates', {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result.enabled).toBe(true);
expect(result.reason).toBe('explicit');
});

it('should check dates before other conditions', () => {
const flags: FeatureFlagsMap = {
'complex-flag': {
Expand All @@ -122,73 +122,73 @@ describe('evaluateFlag', () => {
rolloutPercentage: 100,
},
};

const result = evaluateFlag(flags, 'complex-flag', {
currentTime: '2024-12-15T00:00:00Z',
userEmail: 'test@example.com',
});

expect(result.enabled).toBe(false);
expect(result.reason).toBe('not-yet-enabled');
});
});

describe('existing functionality', () => {
it('should return missing for non-existent flag', () => {
const flags: FeatureFlagsMap = {};
const result = evaluateFlag(flags, 'non-existent');

expect(result.enabled).toBe(false);
expect(result.reason).toBe('missing');
});

it('should respect enabled:false', () => {
const flags: FeatureFlagsMap = {
'disabled-flag': { enabled: false },
};
const result = evaluateFlag(flags, 'disabled-flag');

expect(result.enabled).toBe(false);
expect(result.reason).toBe('disabled');
});

it('should handle allowed users', () => {
const flags: FeatureFlagsMap = {
'user-flag': {
enabled: true,
allowedUsers: ['test@example.com'],
},
};

const allowedResult = evaluateFlag(flags, 'user-flag', {
userEmail: 'test@example.com',
});
expect(allowedResult.enabled).toBe(true);
expect(allowedResult.reason).toBe('allowed-user');

const notAllowedResult = evaluateFlag(flags, 'user-flag', {
userEmail: 'other@example.com',
});
expect(notAllowedResult.enabled).toBe(true);
expect(notAllowedResult.reason).toBe('explicit');
});

it('should handle rollout percentages', () => {
const flags: FeatureFlagsMap = {
'rollout-flag': {
enabled: true,
rolloutPercentage: 50,
},
};

const result1 = evaluateFlag(flags, 'rollout-flag', {
userId: 'user-123',
});

const result2 = evaluateFlag(flags, 'rollout-flag', {
userId: 'user-456',
});

expect(['rollout'].includes(result1.reason)).toBe(true);
expect(['rollout'].includes(result2.reason)).toBe(true);
});
Expand All @@ -211,11 +211,11 @@ describe('flagsSnapshotToBooleans', () => {
disableAfter: '2020-01-01T00:00:00Z',
},
};

const result = flagsSnapshotToBooleans(flags, {
currentTime: '2024-12-15T00:00:00Z',
});

expect(result['past-flag']).toBe(true);
expect(result['future-flag']).toBe(false);
expect(result['expired-flag']).toBe(false);
Expand Down
Loading
Loading