Commit d2e2134c authored by Erran Carey's avatar Erran Carey Committed by Olena Horal-Koretska
Browse files

fix: Hide Duo Agent Platform panel when feature flags are disabled

parent 563c1f17
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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)"
        }
      ]
    },
+22 −0
Original line number Diff line number Diff line
@@ -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);
+135 −1
Original line number Diff line number Diff line
@@ -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');

@@ -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(
@@ -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();
+38 −3
Original line number Diff line number Diff line
@@ -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';
@@ -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 () => {
@@ -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 () => ({}),