Loading desktop.package.json +1 −1 Original line number Diff line number Diff line Loading @@ -896,7 +896,7 @@ "type": "webview", "id": "gl.webview.agentic-tabs", "name": "", "when": "config.gitlab.duoAgentPlatform.enabled" "when": "config.gitlab.duoAgentPlatform.enabled && (gitlab.featureFlags.duo_agentic_chat || gitlab.featureFlags.duo_workflow)" } ] }, Loading src/common/feature_flags/constants.ts +22 −0 Original line number Diff line number Diff line Loading @@ -40,7 +40,29 @@ export const FEATURE_FLAGS_DEFAULT_VALUES = { // PLEASE NOTE: We can only query 20 flags at a time so this list shouldn't grow past that. // https://gitlab.com/gitlab-org/gitlab/-/blob/933b5643feebe1feb471be2652d98497c17bc65b/app/graphql/resolvers/app_config/gitlab_instance_feature_flags_resolver.rb#L7 export enum InstanceFeatureFlag { DuoAgenticChat = 'duo_agentic_chat', DuoWorkflow = 'duo_workflow', } // The milestone where a feature flag was added. Early versions // most likely did not have the feature at all so we can default // to hiding it from the user. export const InstanceFeatureFlagIntroduced = { // https://gitlab.com/gitlab-org/gitlab/-/issues/542441 [InstanceFeatureFlag.DuoAgenticChat]: '18.0.0', // https://gitlab.com/gitlab-org/gitlab/-/issues/468627 [InstanceFeatureFlag.DuoWorkflow]: '17.2.0', }; // The milestone where a feature flag was enabled by default. Later // instance versions should use a application setting, group setting, // project setting, user preference etc. to disable the functionality // which we should prefer to a feature flag when available. export const InstanceFeatureFlagRollout = { // https://gitlab.com/gitlab-org/gitlab/-/issues/542441 [InstanceFeatureFlag.DuoAgenticChat]: '18.2.0', // https://gitlab.com/gitlab-org/gitlab/-/issues/468627 [InstanceFeatureFlag.DuoWorkflow]: '18.2.0', }; export const INSTANCE_FEATURE_FLAGS = Object.values(InstanceFeatureFlag); src/common/feature_flags/instance_feature_flag_service.test.ts +135 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,12 @@ import { InstanceFeatureFlagService, getInstanceFeatureFlagsRequest, } from './instance_feature_flag_service'; import { INSTANCE_FEATURE_FLAGS } from './constants'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag, InstanceFeatureFlagIntroduced, InstanceFeatureFlagRollout, } from './constants'; jest.mock('../utils/extension_configuration'); Loading Loading @@ -66,6 +71,18 @@ describe('InstanceFeatureFlagService', () => { expect(INSTANCE_FEATURE_FLAGS.length).toBeLessThanOrEqual(20); }); it('should include introduced versions for all instance flags', () => { expect(Object.values(INSTANCE_FEATURE_FLAGS)).toEqual( Object.keys(InstanceFeatureFlagIntroduced), ); }); it('should include include feature flags for instance flags being rolled out across instance versions', () => { Object.keys(InstanceFeatureFlagRollout).forEach(flag => expect(Object.values(INSTANCE_FEATURE_FLAGS)).toContain(flag), ); }); describe('with instance flags set', () => { beforeEach(() => { setupFetchHandlers( Loading Loading @@ -136,6 +153,123 @@ describe('InstanceFeatureFlagService', () => { }); }); describe('with rolled out feature flags', () => { it('enabled rolled out feature flags by default', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { // Completely rolled out feature flags are removed from the codebase in a future release. featureFlags: [], }, }, }, { request: versionRequest, response: { version: '999.0.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': true, 'gitlab.featureFlags.duo_workflow': true, }); }); it('disable feature flags when disabled in instance', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [ { name: InstanceFeatureFlag.DuoAgenticChat, enabled: false }, { name: InstanceFeatureFlag.DuoWorkflow, enabled: false }, ], }, }, }, { request: versionRequest, response: { version: '18.1.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': false, }); }); it('disable feature flags that have not been introduced yet', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [{ name: InstanceFeatureFlag.DuoWorkflow, enabled: true }], }, }, }, { request: versionRequest, response: { version: '17.5.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': true, }); }); it('disable feature flags disabled by admin', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [ { name: InstanceFeatureFlag.DuoAgenticChat, enabled: false }, { name: InstanceFeatureFlag.DuoWorkflow, enabled: false }, ], }, }, }, { request: versionRequest, response: { version: '18.2.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': false, }); }); }); describe('when an account changes', () => { it('updates the instance-level feature flag context but not the local', async () => { await featureFlagService.init(); Loading src/common/feature_flags/instance_feature_flag_service.ts +38 −3 Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import { GraphQLRequest } from '../platform/web_ide'; import { GitLabPlatformManager } from '../platform/gitlab_platform'; import { versionRequest } from '../gitlab/check_version'; import { ifVersionGte } from '../utils/if_version_gte'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag } from './constants'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag, InstanceFeatureFlagIntroduced, InstanceFeatureFlagRollout, } from './constants'; import { setFeatureFlagContext } from './utils'; export { InstanceFeatureFlag } from './constants'; Loading Loading @@ -78,7 +83,7 @@ export class InstanceFeatureFlagService implements vscode.Disposable { try { const { version } = await platform.fetchFromApi(versionRequest); return await ifVersionGte<Promise<Record<string, boolean>>>( return await ifVersionGte( version, '17.4.0', async () => { Loading @@ -89,7 +94,37 @@ export class InstanceFeatureFlagService implements vscode.Disposable { } return Object.fromEntries( response.metadata.featureFlags.map(({ name, enabled }) => [name, enabled]), INSTANCE_FEATURE_FLAGS.map(flag => { // If the response contained the specified feature flag use the configured value. const instanceFlag = response.metadata?.featureFlags.find( ({ name }) => name === flag, ); if (instanceFlag?.enabled !== undefined) { return [flag, instanceFlag.enabled]; } // Otherwise the feature flag either: // 1. Is not defined for this instance yet. // 2. Was removed from the codebase (after being enabled by default for a period). return ifVersionGte( version, InstanceFeatureFlagIntroduced[flag], () => { const defaultEnabled = InstanceFeatureFlagRollout[flag]; return [ flag, defaultEnabled && ifVersionGte( version, defaultEnabled, () => true, () => false, ), ]; }, () => [flag, false], ); }), ); }, async () => ({}), Loading Loading
desktop.package.json +1 −1 Original line number Diff line number Diff line Loading @@ -896,7 +896,7 @@ "type": "webview", "id": "gl.webview.agentic-tabs", "name": "", "when": "config.gitlab.duoAgentPlatform.enabled" "when": "config.gitlab.duoAgentPlatform.enabled && (gitlab.featureFlags.duo_agentic_chat || gitlab.featureFlags.duo_workflow)" } ] }, Loading
src/common/feature_flags/constants.ts +22 −0 Original line number Diff line number Diff line Loading @@ -40,7 +40,29 @@ export const FEATURE_FLAGS_DEFAULT_VALUES = { // PLEASE NOTE: We can only query 20 flags at a time so this list shouldn't grow past that. // https://gitlab.com/gitlab-org/gitlab/-/blob/933b5643feebe1feb471be2652d98497c17bc65b/app/graphql/resolvers/app_config/gitlab_instance_feature_flags_resolver.rb#L7 export enum InstanceFeatureFlag { DuoAgenticChat = 'duo_agentic_chat', DuoWorkflow = 'duo_workflow', } // The milestone where a feature flag was added. Early versions // most likely did not have the feature at all so we can default // to hiding it from the user. export const InstanceFeatureFlagIntroduced = { // https://gitlab.com/gitlab-org/gitlab/-/issues/542441 [InstanceFeatureFlag.DuoAgenticChat]: '18.0.0', // https://gitlab.com/gitlab-org/gitlab/-/issues/468627 [InstanceFeatureFlag.DuoWorkflow]: '17.2.0', }; // The milestone where a feature flag was enabled by default. Later // instance versions should use a application setting, group setting, // project setting, user preference etc. to disable the functionality // which we should prefer to a feature flag when available. export const InstanceFeatureFlagRollout = { // https://gitlab.com/gitlab-org/gitlab/-/issues/542441 [InstanceFeatureFlag.DuoAgenticChat]: '18.2.0', // https://gitlab.com/gitlab-org/gitlab/-/issues/468627 [InstanceFeatureFlag.DuoWorkflow]: '18.2.0', }; export const INSTANCE_FEATURE_FLAGS = Object.values(InstanceFeatureFlag);
src/common/feature_flags/instance_feature_flag_service.test.ts +135 −1 Original line number Diff line number Diff line Loading @@ -12,7 +12,12 @@ import { InstanceFeatureFlagService, getInstanceFeatureFlagsRequest, } from './instance_feature_flag_service'; import { INSTANCE_FEATURE_FLAGS } from './constants'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag, InstanceFeatureFlagIntroduced, InstanceFeatureFlagRollout, } from './constants'; jest.mock('../utils/extension_configuration'); Loading Loading @@ -66,6 +71,18 @@ describe('InstanceFeatureFlagService', () => { expect(INSTANCE_FEATURE_FLAGS.length).toBeLessThanOrEqual(20); }); it('should include introduced versions for all instance flags', () => { expect(Object.values(INSTANCE_FEATURE_FLAGS)).toEqual( Object.keys(InstanceFeatureFlagIntroduced), ); }); it('should include include feature flags for instance flags being rolled out across instance versions', () => { Object.keys(InstanceFeatureFlagRollout).forEach(flag => expect(Object.values(INSTANCE_FEATURE_FLAGS)).toContain(flag), ); }); describe('with instance flags set', () => { beforeEach(() => { setupFetchHandlers( Loading Loading @@ -136,6 +153,123 @@ describe('InstanceFeatureFlagService', () => { }); }); describe('with rolled out feature flags', () => { it('enabled rolled out feature flags by default', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { // Completely rolled out feature flags are removed from the codebase in a future release. featureFlags: [], }, }, }, { request: versionRequest, response: { version: '999.0.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': true, 'gitlab.featureFlags.duo_workflow': true, }); }); it('disable feature flags when disabled in instance', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [ { name: InstanceFeatureFlag.DuoAgenticChat, enabled: false }, { name: InstanceFeatureFlag.DuoWorkflow, enabled: false }, ], }, }, }, { request: versionRequest, response: { version: '18.1.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': false, }); }); it('disable feature flags that have not been introduced yet', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [{ name: InstanceFeatureFlag.DuoWorkflow, enabled: true }], }, }, }, { request: versionRequest, response: { version: '17.5.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': true, }); }); it('disable feature flags disabled by admin', async () => { setupFetchHandlers( { request: getInstanceFeatureFlagsRequest(INSTANCE_FEATURE_FLAGS), response: { metadata: { featureFlags: [ { name: InstanceFeatureFlag.DuoAgenticChat, enabled: false }, { name: InstanceFeatureFlag.DuoWorkflow, enabled: false }, ], }, }, }, { request: versionRequest, response: { version: '18.2.0', }, }, ); jest.mocked(vscode.commands.executeCommand).mockClear(); await featureFlagService.init(); expect(getVSCodeContext()).toMatchObject({ 'gitlab.featureFlags.duo_agentic_chat': false, 'gitlab.featureFlags.duo_workflow': false, }); }); }); describe('when an account changes', () => { it('updates the instance-level feature flag context but not the local', async () => { await featureFlagService.init(); Loading
src/common/feature_flags/instance_feature_flag_service.ts +38 −3 Original line number Diff line number Diff line Loading @@ -5,7 +5,12 @@ import { GraphQLRequest } from '../platform/web_ide'; import { GitLabPlatformManager } from '../platform/gitlab_platform'; import { versionRequest } from '../gitlab/check_version'; import { ifVersionGte } from '../utils/if_version_gte'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag } from './constants'; import { INSTANCE_FEATURE_FLAGS, InstanceFeatureFlag, InstanceFeatureFlagIntroduced, InstanceFeatureFlagRollout, } from './constants'; import { setFeatureFlagContext } from './utils'; export { InstanceFeatureFlag } from './constants'; Loading Loading @@ -78,7 +83,7 @@ export class InstanceFeatureFlagService implements vscode.Disposable { try { const { version } = await platform.fetchFromApi(versionRequest); return await ifVersionGte<Promise<Record<string, boolean>>>( return await ifVersionGte( version, '17.4.0', async () => { Loading @@ -89,7 +94,37 @@ export class InstanceFeatureFlagService implements vscode.Disposable { } return Object.fromEntries( response.metadata.featureFlags.map(({ name, enabled }) => [name, enabled]), INSTANCE_FEATURE_FLAGS.map(flag => { // If the response contained the specified feature flag use the configured value. const instanceFlag = response.metadata?.featureFlags.find( ({ name }) => name === flag, ); if (instanceFlag?.enabled !== undefined) { return [flag, instanceFlag.enabled]; } // Otherwise the feature flag either: // 1. Is not defined for this instance yet. // 2. Was removed from the codebase (after being enabled by default for a period). return ifVersionGte( version, InstanceFeatureFlagIntroduced[flag], () => { const defaultEnabled = InstanceFeatureFlagRollout[flag]; return [ flag, defaultEnabled && ifVersionGte( version, defaultEnabled, () => true, () => false, ), ]; }, () => [flag, false], ); }), ); }, async () => ({}), Loading