diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget.vue
index 55e4de8183b3d0642698948783f3b61c618a3e59..28e70cc5636e20c8241306fd520b9b439a6ebde1 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget.vue
@@ -1,7 +1,16 @@
 <script>
-import { GlButton, GlForm, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
+import {
+  GlButton,
+  GlForm,
+  GlLoadingIcon,
+  GlCollapsibleListbox,
+  GlTooltipDirective,
+} from '@gitlab/ui';
 import { isEmpty, debounce } from 'lodash';
 import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { sanitize } from '~/lib/dompurify';
+import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
+import { keysFor } from '~/behaviors/shortcuts/keybindings';
 
 import { s__, __, sprintf } from '~/locale';
 
@@ -19,6 +28,9 @@ export default {
     GlForm,
     GlCollapsibleListbox,
   },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
   props: {
     canUpdate: {
       type: Boolean,
@@ -108,6 +120,11 @@ export default {
       required: false,
       default: __('Search'),
     },
+    shortcut: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
   },
   data() {
     return {
@@ -131,6 +148,22 @@ export default {
         ? sprintf(__(`No %{label}`), { label: this.dropdownLabel.toLowerCase() })
         : this.toggleDropdownText;
     },
+    disableShortcuts() {
+      return shouldDisableShortcuts() || Object.keys(this.shortcut).length === 0;
+    },
+    shortcutDescription() {
+      return this.disableShortcuts ? null : this.shortcut.description;
+    },
+    shortcutKey() {
+      return this.disableShortcuts ? null : keysFor(this.shortcut)[0];
+    },
+    tooltipText() {
+      const description = this.shortcutDescription;
+      const key = this.shortcutKey;
+      return this.disableShortcuts
+        ? null
+        : sanitize(`${description} <kbd class="flat gl-ml-1" aria-hidden=true>${key}</kbd>`);
+    },
   },
   watch: {
     itemValue: {
@@ -198,6 +231,8 @@ export default {
       <gl-loading-icon v-if="updateInProgress" />
       <gl-button
         v-if="canUpdate && !isEditing"
+        v-gl-tooltip.viewport.html
+        :title="tooltipText"
         data-testid="edit-button"
         category="tertiary"
         size="small"
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 73f882bc94678244c593298cf582a615ddeeba5d..9a0c646381e7361ec6bf68fd8a1ef81b382ec5f1 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -10,6 +10,7 @@ import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_
 import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
 import { s__, sprintf, __ } from '~/locale';
 import Tracking from '~/tracking';
+import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
 import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
 import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
 import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
@@ -79,6 +80,7 @@ export default {
       currentUser: null,
       updateInProgress: false,
       localUsers: [],
+      shortcut: ISSUE_MR_CHANGE_ASSIGNEE,
     };
   },
   apollo: {
@@ -337,6 +339,7 @@ export default {
     :header-text="headerText"
     :update-in-progress="updateInProgress"
     :reset-button-label="__('Clear')"
+    :shortcut="shortcut"
     clear-search-on-item-select
     data-testid="work-item-assignees"
     @dropdownShown="onDropdownShown"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 5267b10620d6a51758d04672c59988e427252602..c60c93dee0a8cba2ba7b0be92efcc45e08655226 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -18,6 +18,9 @@ import { TYPENAME_GROUP } from '~/graphql_shared/constants';
 import { isLoggedIn } from '~/lib/utils/common_utils';
 import { WORKSPACE_PROJECT } from '~/issues/constants';
 import { addShortcutsExtension } from '~/behaviors/shortcuts';
+import { sanitize } from '~/lib/dompurify';
+import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
+import { keysFor, ISSUABLE_EDIT_DESCRIPTION } from '~/behaviors/shortcuts/keybindings';
 import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items';
 import {
   i18n,
@@ -462,6 +465,16 @@ export default {
     shouldShowEditButton() {
       return !this.editMode && this.canUpdate;
     },
+    editShortcutKey() {
+      return shouldDisableShortcuts() ? null : keysFor(ISSUABLE_EDIT_DESCRIPTION)[0];
+    },
+    editTooltip() {
+      const description = __('Edit title and description');
+      const key = this.editShortcutKey;
+      return shouldDisableShortcuts()
+        ? description
+        : sanitize(`${description} <kbd class="flat gl-ml-1" aria-hidden=true>${key}</kbd>`);
+    },
     modalCloseButtonClass() {
       return {
         'sm:gl-hidden': !this.error,
@@ -933,6 +946,8 @@ export default {
             <div class="gl-ml-auto gl-mt-1 gl-flex gl-gap-3 gl-self-start">
               <gl-button
                 v-if="shouldShowEditButton"
+                v-gl-tooltip.bottom.html
+                :title="editTooltip"
                 category="secondary"
                 data-testid="work-item-edit-form-button"
                 class="shortcut-edit-wi-description"
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index d48dcae9529c98a646c4aeb53b05e81756b3590b..ec84595eeef4b0dad7bf27f959e14429b57f02a1 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -10,6 +10,7 @@ import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/g
 import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
 import { isScopedLabel } from '~/lib/utils/common_utils';
 import Tracking from '~/tracking';
+import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
 import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
 import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
 import updateNewWorkItemMutation from '../graphql/update_new_work_item.mutation.graphql';
@@ -72,6 +73,7 @@ export default {
       addLabelIds: [],
       labelsCache: [],
       labelsToShowAtTopOfTheListbox: [],
+      shortcut: ISSUABLE_CHANGE_LABEL,
     };
   },
   computed: {
@@ -334,6 +336,7 @@ export default {
     :toggle-dropdown-text="dropdownText"
     :header-text="__('Select labels')"
     :reset-button-label="__('Clear')"
+    :shortcut="shortcut"
     show-footer
     multi-select
     clear-search-on-item-select
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index 26eb8a9abe1bc38884b4a187a27234f293a419c8..8a7fd3ba8aa319246618d6e2e0c9857d35482ca0 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
 import { s__, __ } from '~/locale';
 import { MILESTONE_STATE } from '~/sidebar/constants';
 import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
+import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
 import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
 import groupMilestonesQuery from '~/sidebar/queries/group_milestones.query.graphql';
 import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -68,6 +69,7 @@ export default {
       updateInProgress: false,
       milestones: [],
       localMilestone: this.workItemMilestone,
+      shortcut: ISSUE_MR_CHANGE_MILESTONE,
     };
   },
   computed: {
@@ -235,6 +237,7 @@ export default {
     :toggle-dropdown-text="dropdownText"
     :header-text="__('Select milestone')"
     :reset-button-label="__('Clear')"
+    :shortcut="shortcut"
     data-testid="work-item-milestone"
     @dropdownShown="onDropdownShown"
     @searchStarted="search"
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index f611bfacc527375177341bb287272c0f93ae3b57..ef31153232b4c1544ac2c4ff11fe88d92493f747 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -173,12 +173,14 @@ def extensions_marketplace_view
   end
 
   def build_extensions_marketplace_view(title:, message:)
-    extensions_marketplace_home = "%{linkStart}#{::WebIde::ExtensionMarketplace.marketplace_home_url}%{linkEnd}"
+    marketplace_home_url = ::WebIde::ExtensionMarketplace.marketplace_home_url(user: current_user)
+
+    extensions_marketplace_home = "%{linkStart}#{marketplace_home_url}%{linkEnd}"
     {
       name: 'extensions_marketplace',
       message: format(message, extensions_marketplace_home: extensions_marketplace_home),
       title: title,
-      message_url: WebIde::ExtensionMarketplace.marketplace_home_url,
+      message_url: marketplace_home_url,
       help_link: WebIde::ExtensionMarketplace.help_preferences_url
     }
   end
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index 6f5914c52af4a7fe4ec7b66116bfb2ddf0e4675c..66f0e37814fbb7410708217110426763e511533c 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -19,7 +19,12 @@ class WebHookLog < ApplicationRecord
 
   self.primary_key = :id
 
-  partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
+  # The partitioning definition here has been temporarily moved to config/initializers/postgres_partitioning.rb
+  # so that it does not interact with the changing WebHookLog.table_name as influenced by the
+  # ::Ci::Partitionable::Switch module.
+  # In config/initializers/postgres_partitioning.rb:
+  #   - web_hook_logs_daily is registered for daily partitioning for when the flag is ON
+  #   - web_hook_logs is registered for monthly partitioning for when the flag is OFF
 
   belongs_to :web_hook
 
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 2b0875fd4b14ef7a828f0d2b40402e9ae79b0b02..b448cf5443f2b869b59f25945bd694c548a8b3dd 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -10,7 +10,7 @@
 - rich_text_editor_help_text = s_('Preferences|Type in rich text, as you see it.')
 - @color_modes = Gitlab::ColorModes::available_modes.to_json
 - @themes = Gitlab::Themes::available_themes.to_json
-- extensions_marketplace_url = ::WebIde::ExtensionMarketplace.marketplace_home_url
+- extensions_marketplace_url = ::WebIde::ExtensionMarketplace.marketplace_home_url(user: current_user)
 - data_attributes = { color_modes: @color_modes, themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path, extensions_marketplace_url: extensions_marketplace_url }
 - @force_desktop_expanded_sidebar = true
 
diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb
index 9ec06212e82bf9d004155715379b2b6cfb99b967..638fef511d7d29b4020073d86ec0d6bc683c33ce 100644
--- a/config/initializers/postgres_partitioning.rb
+++ b/config/initializers/postgres_partitioning.rb
@@ -35,8 +35,9 @@
     Gitlab::Database::BackgroundMigration::BatchedJobTransitionLog,
     LooseForeignKeys::DeletedRecord,
     Users::GroupVisit,
-    Users::ProjectVisit,
-    WebHookLog
+    Users::ProjectVisit
+    # WebHookLog is temporarily removed from this list and managed without a model
+    # during the switch from web_hook_logs to web_hook_logs_daily
   ])
 
 if Gitlab.ee?
@@ -84,12 +85,20 @@
 
 # Enable partition management for the backfill table during web_hook_logs partitioning.
 # This way new partitions will be created as the trigger syncs new rows across to this table.
+# We're controlling the table backing WebHookLog with the feature flag web_hook_logs_daily_enabled.
+# So that the feature flag does not interact with the partition manager, register both web_hook_logs tables here,
+# disconnected from the feature flag.
 Gitlab::Database::Partitioning.register_tables(
   [
     {
       limit_connection_names: %i[main],
       table_name: 'web_hook_logs_daily',
       partitioned_column: :created_at, strategy: :daily, retain_for: 14.days
+    },
+    {
+      limit_connection_names: %i[main],
+      table_name: 'web_hook_logs',
+      partitioned_column: :created_at, strategy: :monthly, retain_for: 1.month
     }
   ]
 )
diff --git a/doc/administration/geo/replication/img/geo-ha-diagram_v12.png b/doc/administration/geo/replication/img/geo-ha-diagram_v11_11.png
similarity index 100%
rename from doc/administration/geo/replication/img/geo-ha-diagram_v12.png
rename to doc/administration/geo/replication/img/geo-ha-diagram_v11_11.png
diff --git a/doc/administration/geo/replication/multiple_servers.md b/doc/administration/geo/replication/multiple_servers.md
index 6bc9968b18320b88b3a1b2fac1a58d22da21e3e7..b39456600968b3e9374d7bdeddd54175f9f5909d 100644
--- a/doc/administration/geo/replication/multiple_servers.md
+++ b/doc/administration/geo/replication/multiple_servers.md
@@ -18,7 +18,7 @@ described, it is possible to adapt these instructions to your needs.
 
 ## Architecture overview
 
-![Architecture for running Geo in a multi-node configuration with primary and secondary backend services](img/geo-ha-diagram_v12.png)
+![Architecture for running Geo in a multi-node configuration with primary and secondary backend services](img/geo-ha-diagram_v11_11.png)
 
 _[diagram source - GitLab employees only](https://docs.google.com/drawings/d/1z0VlizKiLNXVVVaERFwgsIOuEgjcUqDTWPdQYsE7Z4c/edit)_
 
diff --git a/doc/administration/geo/secondary_proxy/_index.md b/doc/administration/geo/secondary_proxy/_index.md
index eeef06bc6d615a51b672ce4292fcacd02f42c403..a89b01c792127938d1ac6df6b449dd46d1a621fa 100644
--- a/doc/administration/geo/secondary_proxy/_index.md
+++ b/doc/administration/geo/secondary_proxy/_index.md
@@ -252,7 +252,7 @@ Disabling the proxying feature flag has the following general effects:
 - The secondary site does not proxy HTTP requests to the primary site. Instead, it attempts to serve them itself, or fail.
 - Git requests generally succeed. Git pushes are redirected or proxied to the primary site.
 - Other than Git requests, any HTTP request which may write data fails. Read requests generally succeed.
-- The secondary site UI shows a banner: ![Secondary Site UI Banner for Read-Only](img/secondary_proxy_read_only_v17.8.png)
+- The secondary site UI shows a banner: ![Secondary Site UI Banner for Read-Only](img/secondary_proxy_read_only_v17_8.png)
 
 | Feature / component                                 | Succeed                   | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
 | :-------------------------------------------------- | :------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
diff --git a/doc/administration/geo/secondary_proxy/img/secondary_proxy_read_only_v17.8.png b/doc/administration/geo/secondary_proxy/img/secondary_proxy_read_only_v17_8.png
similarity index 100%
rename from doc/administration/geo/secondary_proxy/img/secondary_proxy_read_only_v17.8.png
rename to doc/administration/geo/secondary_proxy/img/secondary_proxy_read_only_v17_8.png
diff --git a/doc/administration/packages/container_registry_troubleshooting.md b/doc/administration/packages/container_registry_troubleshooting.md
index 5c65bf2beca9ab60b8eabd7cdb698244ba694ed7..7b020f52958d9671273821317a9dc855248a8c61 100644
--- a/doc/administration/packages/container_registry_troubleshooting.md
+++ b/doc/administration/packages/container_registry_troubleshooting.md
@@ -5,19 +5,42 @@ info: To determine the technical writer assigned to the Stage/Group associated w
 title: Troubleshooting the container registry
 ---
 
-Before diving in to the following sections, here's some basic troubleshooting:
+Before investigating specific issues, try these troubleshooting steps:
 
-1. Check to make sure that the system clock on your Docker client and GitLab server have
-   been synchronized (for example, via NTP).
+1. Verify that the system clock on your Docker client and GitLab server are synchronized (for example, through NTP).
 
-1. If you are using an S3-backed Registry, double check that the IAM
-   permissions and the S3 credentials (including region) are correct. See
-   [the sample IAM policy](https://distribution.github.io/distribution/storage-drivers/s3/)
-   for more details.
+1. For S3-backed registries, verify your IAM permissions and S3 credentials (including region) are correct.
+   For more information, see the [sample IAM policy](https://distribution.github.io/distribution/storage-drivers/s3/).
 
-1. Check the Registry logs (for example `/var/log/gitlab/registry/current`) and the GitLab production logs
-   for errors (for example `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
-   there.
+1. Check for errors in the registry logs (for example, `/var/log/gitlab/registry/current`) and the GitLab production logs
+   (for example, `/var/log/gitlab/gitlab-rails/production.log`).
+
+1. Review the NGINX configuration file for the container registry (for example, `/var/opt/gitlab/nginx/conf/gitlab-registry.conf`)
+   to confirm which port receives requests.
+
+1. Verify that requests are correctly forwarded to the container registry:
+
+   ```shell
+   curl --verbose --noproxy "*" https://<hostname>:<port>/v2/_catalog
+   ```
+
+   The response should include a line with `Www-Authenticate: Bearer` containing `service="container_registry"`. For example:
+
+   ```plaintext
+   < HTTP/1.1 401 Unauthorized
+   < Server: nginx
+   < Date: Fri, 07 Mar 2025 08:24:43 GMT
+   < Content-Type: application/json
+   < Content-Length: 162
+   < Connection: keep-alive
+   < Docker-Distribution-Api-Version: registry/2.0
+   < Www-Authenticate: Bearer realm="https://<hostname>/jwt/auth",service="container_registry",scope="registry:catalog:*"
+   < X-Content-Type-Options: nosniff
+   <
+   {"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":
+   [{"Type":"registry","Class":"","Name":"catalog","ProjectPath":"","Action":"*"}]}]}
+   * Connection #0 to host <hostname> left intact
+   ```
 
 ## Using self-signed certificates with container registry
 
diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md
index 8e2d9b161396ef3b3dbcc20b213e80f5195f8d38..328b31bd9e0eb16d3c6c4f24f7269eb4278f0e45 100644
--- a/doc/integration/gmail_action_buttons_for_gitlab.md
+++ b/doc/integration/gmail_action_buttons_for_gitlab.md
@@ -15,8 +15,6 @@ title: Gmail actions
 GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview).
 When you configure this integration, emails that require an action are marked in Gmail.
 
-![Gmail actions button](img/gmail_action_buttons_for_gitlab_v8.png)
-
 To get this functioning, you must be registered with Google. For instructions, see
 [Register with Google](https://developers.google.com/gmail/markup/registering-with-google).
 
diff --git a/doc/integration/img/gmail_action_buttons_for_gitlab_v8.png b/doc/integration/img/gmail_action_buttons_for_gitlab_v8.png
deleted file mode 100644
index 0e3e24d6ffc6cb95228d2bf0300f787cdc4f580d..0000000000000000000000000000000000000000
Binary files a/doc/integration/img/gmail_action_buttons_for_gitlab_v8.png and /dev/null differ
diff --git a/doc/user/application_security/dast/browser/configuration/customize_settings.md b/doc/user/application_security/dast/browser/configuration/customize_settings.md
index 21fc39eaa2b093ea0da87cca18b3e6ac6c2c5542..2ab2920e8f85920cf83549d87b55011718fbe7f1 100644
--- a/doc/user/application_security/dast/browser/configuration/customize_settings.md
+++ b/doc/user/application_security/dast/browser/configuration/customize_settings.md
@@ -181,7 +181,7 @@ each step in the process:
    1. Parse content and renders the initial page.
    1. Trigger the standard "document ready" event.
 
-  This phase uses either `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` (for full page loads) or `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` (for in-page actions), which sets the maximum wait time for document loading.
+   This phase uses either `DAST_PAGE_READY_AFTER_NAVIGATION_TIMEOUT` (for full page loads) or `DAST_PAGE_READY_AFTER_ACTION_TIMEOUT` (for in-page actions), which sets the maximum wait time for document loading.
 
 1. **Client-Side rendering**: After initial loading, many single-page applications:
 
@@ -190,7 +190,7 @@ each step in the process:
    - Render a DOM and performs updates based on fetched data (`DAST_PAGE_DOM_READY_TIMEOUT`).
    - Display page loading indicators (`DAST_PAGE_IS_LOADING_ELEMENT`).
 
-  The scanner monitors these activities to determine when the page is ready for interaction.
+   The scanner monitors these activities to determine when the page is ready for interaction.
 
 The following chart illustrates the sequence timeouts used when crawling a page:
 
diff --git a/doc/user/gitlab_duo/tutorials/fix_code_python_shop.md b/doc/user/gitlab_duo/tutorials/fix_code_python_shop.md
new file mode 100644
index 0000000000000000000000000000000000000000..c701d42f29dde6366ee9f52cea8204e5fc089de7
--- /dev/null
+++ b/doc/user/gitlab_duo/tutorials/fix_code_python_shop.md
@@ -0,0 +1,1414 @@
+---
+stage: none
+group: Tutorials
+description: Tutorial on how to create a shop application in Python with GitLab Duo.
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Tutorial: Use GitLab Duo to create a shop application in Python
+
+<!-- vale gitlab_base.FutureTense = NO -->
+
+You have been hired as a developer at an online bookstore. The current system for
+managing inventory is a mix of spreadsheets and manual processes, leading to inventory
+errors and delayed updates. Your team needs to create a web application that can:
+
+- Track book inventory in real time.
+- Enable staff to add new books as they arrive.
+- Prevent common data entry errors, like negative prices or quantities.
+- Provide a foundation for future customer-facing features.
+
+This tutorial guides you through creating and debugging a [Python](https://www.python.org/)
+web application with a database backend that meets these requirements.
+
+You'll use [GitLab Duo Chat](../../gitlab_duo_chat/_index.md)
+and [GitLab Duo Code Suggestions](../../project/repository/code_suggestions/_index.md)
+to help you:
+
+- Set up an organized Python project with standard directories and essential files.
+- Configure the Python virtual environment.
+- Install the [Flask](https://flask.palletsprojects.com/en/stable/) framework as the foundation
+  for the web application.
+- Install required dependencies, and prepare the project for development.
+- Set up the Python configuration file and environment variables for Flask application
+  development.
+- Implement core features, including article models, database operations,
+  API routes, and inventory management features.
+- Test that the application works as intended, comparing your code with example code files.
+
+## Before you begin
+
+- [Install the latest version of Python](https://www.python.org/downloads/) on your system.
+  You can ask Chat how to do that for your operating system.
+- Make sure your organization has purchased a
+  [GitLab Duo add-on subscription (either Duo Pro or Duo Enterprise)](https://about.gitlab.com/gitlab-duo/#pricing),
+  and your administrator has [assigned you a seat](../../../subscriptions/subscription-add-ons.md#assign-gitlab-duo-seats).
+- Install an extension in your preferred IDE:
+  - [Web IDE](../../project/web_ide/_index.md): Access through your GitLab instance
+  - [VS Code](../../../editor_extensions/visual_studio_code/setup.md)
+  - [Visual Studio](../../../editor_extensions/visual_studio/setup.md)
+  - [JetBrains IDE](../../../editor_extensions/jetbrains_ide/_index.md)
+  - [Neovim](../../../editor_extensions/neovim/setup.md)
+- Authenticate with GitLab from the IDE, using either
+  [OAuth](../../../integration/google.md) or a
+  [personal access token with the `api` scope](../../profile/personal_access_tokens.md#create-a-personal-access-token).
+
+## Use GitLab Duo Chat and Code Suggestions
+
+In this tutorial, you will use Chat and Code Suggestions to create the Python
+web application. Multiple ways exist to use these features.
+
+### Use GitLab Duo Chat
+
+You can use Chat in the GitLab UI, the Web IDE, or in your IDE.
+
+#### Use Chat in the GitLab UI
+
+1. In the upper-right corner, select **GitLab Duo Chat**. A drawer opens on the
+   right side of your browser tab.
+1. Enter your question in the chat input box. Press **Enter**, or select **Send**.
+   It might take a few seconds for the interactive AI chat to produce an answer.
+
+#### Use Chat in the Web IDE
+
+1. Open the Web IDE:
+   1. In the GitLab UI, on the left sidebar, select **Search or go to** and find your project.
+   1. Select a file. Then in the upper right, select **Edit > Open in Web IDE**.
+1. Open Chat by using one of these methods:
+   - On the left sidebar, select **GitLab Duo Chat**.
+   - In the file that you have open in the editor, select some code.
+     1. Right-click and select **GitLab Duo Chat**.
+     1. Select **Explain selected code**, **Generate Tests**, or **Refactor**.
+   - Use the keyboard shortcut: <kbd>ALT</kbd>+<kbd>d</kbd> (on Windows and Linux) or
+     <kbd>Option</kbd>+<kbd>d</kbd> (on Mac).
+1. In the message box, enter your question. Press **Enter**, or select **Send**.
+
+#### Use Chat in your IDE
+
+How you use Chat in your IDE differs depending on which IDE you use.
+
+{{< tabs >}}
+
+{{< tab title="VS Code" >}}
+
+1. In VS Code, open a file. The file does not need to be a file in a Git repository.
+1. On the left sidebar, select **GitLab Duo Chat** ({{< icon name="duo-chat" >}}).
+1. In the message box, enter your question. Press **Enter**, or select **Send**.
+1. In the chat pane, on the top right corner, select **Show Status** to show information
+   in the Command Palette.
+
+You can also interact with Duo Chat while you're working with a subset of code.
+
+1. In VS Code, open a file. The file does not need to be a file in a Git repository.
+1. In the file, select some code.
+1. Right-click and select **GitLab Duo Chat**.
+1. Select an option, or **Open Quick Chat** and ask a question, like
+   `Can you simplify this code?` and press <kbd>Enter</kbd>.
+
+For more information, see [Use GitLab Duo Chat in VS Code](../../gitlab_duo_chat/_index.md#use-gitlab-duo-chat-in-vs-code).
+
+{{< /tab >}}
+
+{{< tab title="JetBrains IDEs" >}}
+
+1. Open a project in a JetBrains IDE that supports Python, such as
+   [PyCharm](https://www.jetbrains.com/pycharm/), or [IntelliJ IDEA](https://www.jetbrains.com/idea/).
+1. Open GitLab Duo Chat in either a [chat window](../../gitlab_duo_chat/_index.md#in-a-chat-window)
+   or an [editor window](../../gitlab_duo_chat/_index.md#in-the-editor-window).
+
+For more information, see [Use GitLab Duo Chat in JetBrains IDEs](../../gitlab_duo_chat/_index.md#use-gitlab-duo-chat-in-jetbrains-ides).
+
+{{< /tab >}}
+
+{{< /tabs >}}
+
+### Use Code Suggestions
+
+To use Code Suggestions:
+
+1. Open your Git project in a
+   [supported IDE](../../project/repository/code_suggestions/supported_extensions.md#supported-editor-extensions).
+1. Add the project as a remote of your local repository using
+   [`git remote add`](../../../topics/git/commands.md#git-remote-add).
+1. Add your project directory, including the hidden `.git/` folder, to your IDE workspace or project.
+1. Author your code.
+   As you type, suggestions are displayed. Code Suggestions provides code snippets
+   or completes the current line, depending on the cursor position.
+
+1. Describe the requirements in natural language.
+   Code Suggestions generates functions and code snippets based on the context provided.
+
+1. When you receive a suggestion, you can do any of the following:
+   - To accept a suggestion, press <kbd>Tab</kbd>.
+   - To accept a partial suggestion, press either <kbd>Control</kbd>+<kbd>Right arrow</kbd> or
+     <kbd>Command</kbd>+<kbd>Right arrow</kbd>.
+   - To reject a suggestion, press <kbd>Esc</kbd>.
+   - To ignore a suggestion, keep typing as you usually would.
+
+For more information, see the [Code Suggestions documentation](../../project/repository/code_suggestions/_index.md).
+
+Now that you know how to use Chat and Code Suggestions, let's start building the
+web application. First, you will create an organized Python project structure.
+
+## Create the project structure
+
+To start with, you need a well-organized project structure that follows
+Python best practices. A proper structure makes your code more maintainable, testable,
+and easier for other developers to understand.
+
+You can use Chat to help you understand Python project organization conventions
+and generate the appropriate files. This saves you time researching best practices, and
+ensures you do not miss critical components.
+
+1. Open Chat in your IDE and enter:
+
+   ```plaintext
+   What is the recommended project structure for a Python web application? Include
+   common files, and explain the purpose of each file.
+   ```
+
+   This prompt helps you understand Python project organization before creating files.
+
+1. Create a new folder for the Python project and create a directory and file structure
+   based on Chat's response. It will probably be similar to the following:
+
+   ```plaintext
+   python-shop-app/
+   ├── LICENSE
+   ├── README.md
+   ├── requirements.txt
+   ├── setup.py
+   ├── .gitignore
+   ├── .env
+   ├── app/
+   │   ├── __init__.py
+   │   ├── models/
+   │   │   ├── __init__.py
+   │   │   └── article.py
+   │   ├── routes/
+   │   │   ├── __init__.py
+   │   │   └── shop.py
+   │   └── database.py
+   └── tests/
+       ├── __init__.py
+       └── test_shop.py
+   ```
+
+1. You must populate the `.gitignore` file. Enter the following into Chat:
+
+   ```plaintext
+   Generate a .gitignore file for a Python project that uses Flask, SQLite, and
+   virtual environments. Include common IDE files.
+   ```
+
+1. Copy the response into the `.gitignore` file.
+
+1. For the `README` file, enter the following into Chat:
+
+   ```plaintext
+   Generate a README.md file for a Python web application that manages a bookstore
+   inventory. Make sure that it includes all sections for requirements, setup, and usage.
+   ```
+
+You have now created a properly-structured Python project that follows industry
+best practices. This organization makes your code easier to maintain and test.
+Next, you'll set up your development environment to start writing code.
+
+## Set up the development environment
+
+A properly isolated development environment prevents dependency conflicts and makes
+your application deployable.
+
+You will use Chat to help you set up a Python virtual environment and create a
+`requirements.txt` file with the right dependencies. This ensures you have a stable
+foundation for development.
+
+```plaintext
+   python-shop-app/
+   ├── LICENSE
+   ├── README.md
+   ├── requirements.txt <= File you are updating
+   ├── setup.py
+   ├── .gitignore
+   ├── .env
+   ├── app/
+   │   ├── __init__.py
+   │   ├── models/
+   │   │   ├── __init__.py
+   │   │   └── article.py
+   │   ├── routes/
+   │   │   ├── __init__.py
+   │   │   └── shop.py
+   │   └── database.py
+   └── tests/
+       ├── __init__.py
+       └── test_shop.py
+```
+
+1. Optional. Ask Chat about how Python and Flask work together to produce
+   web applications.
+
+1. Use Chat to understand the best practices for setting up a Python environment:
+
+   ```plaintext
+   What are the recommended steps for setting up a Python virtual environment with
+   Flask? Include information about requirements.txt and pip.
+   ```
+
+   Ask any follow-up questions that you need to. For example:
+
+   ```plaintext
+   What does the requirements.txt do in a Python web app?
+   ```
+
+1. Based on the response, first create and activate a virtual environment
+   (for example, on MacOS using Homebrew's `python3` package):
+
+   ```plaintext
+   python3 -m venv myenv
+   source myenv/bin/activate
+   ```
+
+1. You must also create a `requirements.txt` file. Ask Chat the following:
+
+   ```plaintext
+   What should be included in requirements.txt for a Flask web application with
+   SQLite database and testing capabilities? Include specific version numbers.
+   ```
+
+   Copy the response to the `requirements.txt` file.
+
+1. Install the dependencies named in the `requirements.txt` file:
+
+   ```plaintext
+   pip install -r requirements.txt
+   ```
+
+Your development environment is now configured with all necessary dependencies
+isolated in a virtual environment to prevent conflicts. Next, you'll configure
+the project's package and environment settings.
+
+## Configure the project
+
+Proper configuration, including environment variables, enables your application
+to run consistently across different environments.
+
+You'll use Code Suggestions to help generate and refine the configuration.
+Then, you'll ask Chat to explain the purpose of each setting, so you understand
+what you're configuring and why.
+
+1. You have already created a Python configuration file called `setup.py` in your
+   project folder:
+
+   ```plaintext
+      python-shop-app/
+      ├── LICENSE
+      ├── README.md
+      ├── requirements.txt
+      ├── setup.py <= File you are updating
+      ├── .gitignore
+      ├── .env
+      ├── app/
+      │   ├── __init__.py
+      │   ├── models/
+      │   │   ├── __init__.py
+      │   │   └── article.py
+      │   ├── routes/
+      │   │   ├── __init__.py
+      │   │   └── shop.py
+      │   └── database.py
+      └── tests/
+          ├── __init__.py
+          └── test_shop.py
+   ```
+
+   Open this file, and enter this comment at the top of the file:
+
+   ```plaintext
+   # Populate this setup.py configuration file for a Flask web application
+   # Include dependencies for Flask, testing, and database functionality
+   # Use semantic versioning
+   ```
+
+   Code Suggestions generates the configuration for you.
+
+1. Optional. Select the generated configuration code and use the following
+   [slash commands](../../gitlab_duo_chat/examples.md#gitlab-duo-chat-slash-commands):
+
+   - Use [`/explain`](../../gitlab_duo_chat/examples.md#explain-selected-code)
+     to understand what each configuration setting does.
+   - Use [`/refactor`](../../gitlab_duo_chat/examples.md#refactor-code-in-the-ide)
+     to identify potential improvements in the configuration structure.
+
+1. Review and adjust the generated code as needed.
+
+   If you are not sure what you can adjust in the configuration file, ask Chat.
+
+   If you want to ask Chat what to adjust, do so in the IDE in the `setup.py`
+   file, instead of in the GitLab UI. This provides Chat with
+   [the context you're working in](../../gitlab_duo_chat/_index.md#the-context-chat-is-aware-of),
+   including the `setup.py` file you just created.
+
+   ```plaintext
+   You have used Code Suggestions to generate a Python configuration file, `setup.py`,
+   for a Flask web application. This file includes dependencies for Flask, testing,
+   and database functionality. If I were to review this file, what might I want
+   to change and adjust?
+   ```
+
+1. Save the file.
+
+### Set the environment variables
+
+Next, you're going to use both Chat and Code Suggestions to set the environment variables.
+
+1. In Chat, ask the following:
+
+   ```plaintext
+   In a Python project, what environment variables should be set for a Flask application in development mode? Include database configuration.
+   ```
+
+1. You have already created a `.env` file to store environment variables.
+
+   ```plaintext
+      python-shop-app/
+      ├── LICENSE
+      ├── README.md
+      ├── requirements.txt
+      ├── setup.py
+      ├── .gitignore
+      ├── .env <= File you are updating
+      ├── app/
+      │   ├── __init__.py
+      │   ├── models/
+      │   │   ├── __init__.py
+      │   │   └── article.py
+      │   ├── routes/
+      │   │   ├── __init__.py
+      │   │   └── shop.py
+      │   └── database.py
+      └── tests/
+          ├── __init__.py
+          └── test_shop.py
+   ```
+
+   Open this file, and enter the following comment at the top of the file, including
+   the environment variables that Chat recommended:
+
+   ```plaintext
+   # Populate this .env file to store environment variables
+   # Include the following
+   # ...
+   # Use semantic versioning
+   ```
+
+1. Review and adjust the generated code as needed, and save the file.
+
+You have configured your project and set the environment variables. This ensures
+your application can be deployed consistently across different environments. Next,
+you'll create the application code for the inventory system.
+
+## Create the application code
+
+The Flask web framework has three core components:
+
+- Models: Contains the data and business logic, and the database model. Specified in the `article.py` file.
+- Views: Handles HTTP requests and responses. Specified in the `shop.py` file.
+- Controller: Manages data storage and retrieval. Specified in the `database.py` file.
+
+You will use Chat and Code Suggestions to help you define each of these three components
+in three files in your Python project structure:
+
+- `article.py` defines the models component, specifically the database model.
+- `shop.py` defines the views component, specifically the API routes.
+- `database.py` defines the controller component.
+
+### Create the article file to define the database model
+
+Your bookstore needs database models and operations to manage inventory effectively.
+
+To create the application code for the bookstore inventory system, you will
+use an article file to define the database model for articles.
+
+You will use Code Suggestions to help generate the code, and Chat to
+implement best practices for data modeling and database management.
+
+1. You have already created an `article.py` file:
+
+   ```plaintext
+      python-shop-app/
+      ├── LICENSE
+      ├── README.md
+      ├── requirements.txt
+      ├── setup.py
+      ├── .gitignore
+      ├── .env
+      ├── app/
+      │   ├── __init__.py
+      │   ├── models/
+      │   │   ├── __init__.py
+      │   │   └── article.py <= File you are updating
+      │   ├── routes/
+      │   │   ├── __init__.py
+      │   │   └── shop.py
+      │   └── database.py
+      └── tests/
+          ├── __init__.py
+          └── test_shop.py
+   ```
+
+   In this file, use Code Suggestions
+   and enter the following:
+
+   ```plaintext
+   # Create an Article class for a bookstore inventory system
+   # Include fields for: name, price, quantity
+   # Add data validation for each field
+   # Add methods to convert to/from dictionary format
+   ```
+
+1. Optional. Use the following [slash commands](../../gitlab_duo_chat/examples.md#gitlab-duo-chat-slash-commands):
+
+   - Use [`/explain`](../../gitlab_duo_chat/examples.md#explain-selected-code)
+     to understand how the article class works and its design patterns.
+   - Use, [`/refactor`](../../gitlab_duo_chat/examples.md#refactor-code-in-the-ide)
+     to identify potential improvements in the class structure and methods.
+
+1. Review and adjust the generated code as needed, and save the file.
+
+Next you will define the API routes.
+
+### Create the shop file to define the API routes
+
+Now that you have created the article file to define the database model, you will
+create the API routes.
+
+API routes are crucial for a web application because they:
+
+- Define the public API for clients to interact with your application.
+- Map HTTP requests to the appropriate code in your application.
+- Handle input validation and error responses.
+- Transform data between your internal models and the JSON format expected by API clients.
+
+For your bookstore inventory system, these routes will allow staff to:
+
+- View all books in inventory.
+- Look up specific books by ID.
+- Add new books as they arrive.
+- Update book information such as price or quantity.
+- Remove books that are no longer needed.
+
+In Flask, routes are functions that handle requests to specific URL endpoints.
+For example, a route for `GET /books` would return a list of all books, while
+`POST /books` would add a new book to the inventory.
+
+You will use Chat and Code Suggestions to create these routes in the `shop.py`
+file that you've already set up in your project structure:
+
+```plaintext
+python-shop-app/
+├── LICENSE
+├── README.md
+├── requirements.txt
+├── setup.py
+├── .gitignore
+├── .env
+├── app/
+│   ├── __init__.py
+│   ├── models/
+│   │   ├── __init__.py
+│   │   └── article.py
+│   ├── routes/
+│   │   ├── __init__.py
+│   │   └── shop.py <= File you are updating
+│   └── database.py
+└── tests/
+   ├── __init__.py
+   └── test_shop.py
+```
+
+#### Create the Flask application and routes
+
+1. Open the `shop.py` file. To use Code Suggestions, enter this comment
+   at the top of the file:
+
+  ```plaintext
+  # Create Flask routes for a bookstore inventory system
+  # Include routes for:
+  # - Getting all books (GET /books)
+  # - Getting a single book by ID (GET /books/<id>)
+  # - Adding a new book (POST /books)
+  # - Updating a book (PUT /books/<id>)
+  # - Deleting a book (DELETE /books/<id>)
+  # Use the Article class from models.article and database from database.py
+  # Include proper error handling and HTTP status codes
+  ```
+
+1. Review the generated code. It should include:
+
+   - Import statements for Flask, request, and `jsonify`.
+   - Import statements for your Article class and database module.
+   - Route definitions for all CRUD operations (Create, Read, Update, Delete).
+   - Proper error handling and HTTP status codes.
+
+1. Optional. Use these slash commands:
+
+   - Use [`/explain`](../../gitlab_duo_chat/examples.md#explain-selected-code)
+     to understand how the Flask routing works.
+   - Use [`/refactor`](../../gitlab_duo_chat/examples.md#refactor-code-in-the-ide)
+     to identify potential improvements.
+
+1. If the generated code doesn't fully meet your needs, or you want to understand
+   how to improve it, you can ask Chat from within the `shop.py` file:
+
+  ```plaintext
+  Can you suggest improvements for my Flask routes in this shop.py file?
+  I want to ensure that:
+  1. The routes follow RESTful API design principles
+  2. Responses include appropriate HTTP status codes
+  3. Input validation is handled properly
+  4. The code follows Flask best practices
+  ```
+
+1. You also need to create the Flask application instance in the `__init__.py`
+   file inside the `app` directory. Open this file and use Code Suggestions to
+   generate the appropriate code:
+
+  ```plaintext
+  # Create a Flask application factory
+  # Configure the app with settings from environment variables
+  # Register the shop blueprint
+  # Return the configured app
+  ```
+
+1. Save both files.
+
+### Create the database file to manage data storage and retrieval
+
+Finally, you will create the database operations code. You have already created
+a `database.py` file:
+
+```plaintext
+   python-shop-app/
+   ├── LICENSE
+   ├── README.md
+   ├── requirements.txt
+   ├── setup.py
+   ├── .gitignore
+   ├── .env
+   ├── app/
+   │   ├── __init__.py
+   │   ├── models/
+   │   │   ├── __init__.py
+   │   │   └── article.py
+   │   ├── routes/
+   │   │   ├── __init__.py
+   │   │   └── shop.py
+   │   └── database.py <= File you are updating
+   └── tests/
+       ├── __init__.py
+       └── test_shop.py
+```
+
+1. Enter the following into Chat:
+
+   ```plaintext
+   Generate a Python class that manages SQLite database operations for a bookstore inventory. Include:
+   - Context manager for connections
+   - Table creation
+   - CRUD operations
+   - Error handling
+   Show the complete code with comments.
+   ```
+
+1. Review and adjust the generated code as needed, and save the file.
+
+You have successfully created the foundational code for your inventory
+management system and defined the core components
+of a Python web application built using the Flask framework.
+
+Next you'll check your created code against example code files.
+
+## Check your code against example code files
+
+The following examples show complete, working code that should be similar to the
+code you end up with after following the tutorial.
+
+{{< tabs >}}
+
+{{< tab title=".gitignore" >}}
+
+This file shows standard Python project exclusions:
+
+```plaintext
+# Virtual Environment
+myenv/
+venv/
+ENV/
+env/
+.venv/
+
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# SQLite database files
+*.db
+*.sqlite
+*.sqlite3
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+
+# IDE specific files
+.idea/
+.vscode/
+*.swp
+*.swo
+.DS_Store
+```
+
+{{< /tab >}}
+
+{{< tab title="README.md" >}}
+
+A comprehensive `README` file with setup and usage instructions.
+
+```markdown
+# Bookstore Inventory Management System
+
+A Python web application for managing bookstore inventory, built with Flask and SQLite.
+
+## Features
+
+- Track book inventory in real time.
+- Add, update, and remove books.
+- Data validation to prevent common errors.
+- RESTful API for inventory management.
+
+## Requirements
+
+- Python 3.8 or higher.
+- Flask 2.2.0 or higher.
+- SQLite 3.
+
+## Installation
+
+1. Clone the repository:
+
+   ```shell
+   git clone https://gitlab.com/your-username/python-shop-app.git
+   cd python-shop-app
+   ```
+
+1. Create and activate a virtual environment:
+
+   ```shell
+   python -m venv myenv
+   source myenv/bin/activate  # On Windows: myenv\Scripts\activate
+   ```
+
+1. Install dependencies:
+
+   ```shell
+   pip install -r requirements.txt
+   ```
+
+1. Set up environment variables:
+
+   Copy `.env.example` to `.env` and modify as needed.
+
+## Usage
+
+1. Start the Flask application:
+
+   ```shell
+   flask run
+   ```
+
+1. The API will be available at `http://localhost:5000/`
+
+## API Endpoints
+
+- `GET /books` - Get all books
+- `GET /books/<id>` - Get a specific book
+- `POST /books` - Add a new book
+- `PUT /books/<id>` - Update a book
+- `DELETE /books/<id>` - Delete a book
+
+## Testing
+
+Run tests with `pytest`:
+
+```python
+python -m pytest
+```
+
+{{< /tab >}}
+
+{{< tab title="requirements.txt" >}}
+
+Lists all required Python packages with versions.
+
+```plaintext
+Flask==2.2.3
+pytest==7.3.1
+pytest-flask==1.2.0
+Flask-SQLAlchemy==3.0.3
+SQLAlchemy==2.0.9
+python-dotenv==1.0.0
+Werkzeug==2.2.3
+requests==2.28.2
+```
+
+{{< /tab >}}
+
+{{< tab title="setup.py" >}}
+
+Project configuration for packaging.
+
+```python
+from setuptools import setup, find_packages
+
+setup(
+    name="bookstore-inventory",
+    version="0.1.0",
+    packages=find_packages(),
+    include_package_data=True,
+    install_requires=[
+        "Flask>=2.2.0",
+        "Flask-SQLAlchemy>=3.0.0",
+        "SQLAlchemy>=2.0.0",
+        "pytest>=7.0.0",
+        "pytest-flask>=1.2.0",
+        "python-dotenv>=1.0.0",
+    ],
+    python_requires=">=3.8",
+    author="Your Name",
+    author_email="your.email@example.com",
+    description="A Flask web application for managing bookstore inventory",
+    keywords="flask, inventory, bookstore",
+    url="https://gitlab.com/your-username/python-shop-app",
+    classifiers=[
+        "Development Status :: 3 - Alpha",
+        "Environment :: Web Environment",
+        "Framework :: Flask",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+    ],
+)
+```
+
+{{< /tab >}}
+
+{{< tab title=".env" >}}
+
+Contains environment variables for the application.
+
+```plaintext
+# Flask configuration
+FLASK_APP=app
+FLASK_ENV=development
+FLASK_DEBUG=1
+SECRET_KEY=your-secret-key-change-in-production
+
+# Database configuration
+DATABASE_URL=sqlite:///bookstore.db
+TEST_DATABASE_URL=sqlite:///test_bookstore.db
+
+# Application settings
+BOOK_TITLE_MAX_LENGTH=100
+MAX_PRICE=1000.00
+MAX_QUANTITY=1000
+```
+
+{{< /tab >}}
+
+{{< tab title="app/models/article.py" >}}
+
+Article class with full validation.
+
+```python
+class Article:
+    """Article class for a bookstore inventory system."""
+
+    def __init__(self, name, price, quantity, article_id=None):
+        """
+        Initialize an article with validation.
+
+        Args:
+            name (str): The name/title of the book
+            price (float): The price of the book
+            quantity (int): The quantity in stock
+            article_id (int, optional): The unique identifier for the article
+
+        Raises:
+            ValueError: If any of the fields fail validation
+        """
+        self.id = article_id
+        self.set_name(name)
+        self.set_price(price)
+        self.set_quantity(quantity)
+
+    def set_name(self, name):
+        """
+        Set the name with validation.
+
+        Args:
+            name (str): The name/title of the book
+
+        Raises:
+            ValueError: If name is empty or too long
+        """
+        if not name or not isinstance(name, str):
+            raise ValueError("Book title cannot be empty and must be a string")
+
+        if len(name) > 100:  # Max length validation
+            raise ValueError("Book title cannot exceed 100 characters")
+
+        self.name = name.strip()
+
+    def set_price(self, price):
+        """
+        Set the price with validation.
+
+        Args:
+            price (float): The price of the book
+
+        Raises:
+            ValueError: If price is negative or not a number
+        """
+        try:
+            price_float = float(price)
+        except (ValueError, TypeError):
+            raise ValueError("Price must be a number")
+
+        if price_float < 0:
+            raise ValueError("Price cannot be negative")
+
+        if price_float > 1000:  # Max price validation
+            raise ValueError("Price cannot exceed 1000")
+
+        # Ensure price has at most 2 decimal places
+        self.price = round(price_float, 2)
+
+    def set_quantity(self, quantity):
+        """
+        Set the quantity with validation.
+
+        Args:
+            quantity (int): The quantity in stock
+
+        Raises:
+            ValueError: If quantity is negative or not an integer
+        """
+        try:
+            quantity_int = int(quantity)
+        except (ValueError, TypeError):
+            raise ValueError("Quantity must be an integer")
+
+        if quantity_int < 0:
+            raise ValueError("Quantity cannot be negative")
+
+        if quantity_int > 1000:  # Max quantity validation
+            raise ValueError("Quantity cannot exceed 1000")
+
+        self.quantity = quantity_int
+
+    def to_dict(self):
+        """
+        Convert the article to a dictionary.
+
+        Returns:
+            dict: Dictionary representation of the article
+        """
+        return {
+            "id": self.id,
+            "name": self.name,
+            "price": self.price,
+            "quantity": self.quantity
+        }
+
+    @classmethod
+    def from_dict(cls, data):
+        """
+        Create an article from a dictionary.
+
+        Args:
+            data (dict): Dictionary with article data
+
+        Returns:
+            Article: New article instance
+        """
+        article_id = data.get("id")
+        return cls(
+            name=data["name"],
+            price=data["price"],
+            quantity=data["quantity"],
+            article_id=article_id
+        )
+```
+
+{{< /tab >}}
+
+{{< tab title="app/routes/shop.py" >}}
+
+Complete API endpoints with error handling.
+
+```python
+from flask import Blueprint, request, jsonify, current_app
+from app.models.article import Article
+from app import database
+import logging
+
+# Create a blueprint for the shop routes
+shop_bp = Blueprint('shop', __name__, url_prefix='/books')
+
+# Set up logging
+logger = logging.getLogger(__name__)
+
+@shop_bp.route('', methods=['GET'])
+def get_all_books():
+    """Get all books from the inventory."""
+    try:
+        books = database.get_all_articles()
+        return jsonify([book.to_dict() for book in books]), 200
+    except Exception as e:
+        logger.error(f"Error getting all books: {str(e)}")
+        return jsonify({"error": "Failed to retrieve books"}), 500
+
+@shop_bp.route('/<int:book_id>', methods=['GET'])
+def get_book(book_id):
+    """Get a specific book by ID."""
+    try:
+        book = database.get_article_by_id(book_id)
+        if book:
+            return jsonify(book.to_dict()), 200
+        return jsonify({"error": f"Book with ID {book_id} not found"}), 404
+    except Exception as e:
+        logger.error(f"Error getting book {book_id}: {str(e)}")
+        return jsonify({"error": f"Failed to retrieve book {book_id}"}), 500
+
+@shop_bp.route('', methods=['POST'])
+def add_book():
+    """Add a new book to the inventory."""
+    data = request.get_json()
+
+    if not data:
+        return jsonify({"error": "No data provided"}), 400
+
+    required_fields = ['name', 'price', 'quantity']
+    for field in required_fields:
+        if field not in data:
+            return jsonify({"error": f"Missing required field: {field}"}), 400
+
+    try:
+        # Validate data by creating an Article object
+        new_book = Article(
+            name=data['name'],
+            price=data['price'],
+            quantity=data['quantity']
+        )
+
+        # Save to database
+        book_id = database.add_article(new_book)
+
+        # Return the created book
+        created_book = database.get_article_by_id(book_id)
+        return jsonify(created_book.to_dict()), 201
+
+    except ValueError as e:
+        return jsonify({"error": str(e)}), 400
+    except Exception as e:
+        logger.error(f"Error adding book: {str(e)}")
+        return jsonify({"error": "Failed to add book"}), 500
+
+@shop_bp.route('/<int:book_id>', methods=['PUT'])
+def update_book(book_id):
+    """Update an existing book."""
+    data = request.get_json()
+
+    if not data:
+        return jsonify({"error": "No data provided"}), 400
+
+    try:
+        # Check if book exists
+        existing_book = database.get_article_by_id(book_id)
+        if not existing_book:
+            return jsonify({"error": f"Book with ID {book_id} not found"}), 404
+
+        # Update book properties
+        if 'name' in data:
+            existing_book.set_name(data['name'])
+        if 'price' in data:
+            existing_book.set_price(data['price'])
+        if 'quantity' in data:
+            existing_book.set_quantity(data['quantity'])
+
+        # Save updated book
+        database.update_article(existing_book)
+
+        # Return the updated book
+        updated_book = database.get_article_by_id(book_id)
+        return jsonify(updated_book.to_dict()), 200
+
+    except ValueError as e:
+        return jsonify({"error": str(e)}), 400
+    except Exception as e:
+        logger.error(f"Error updating book {book_id}: {str(e)}")
+        return jsonify({"error": f"Failed to update book {book_id}"}), 500
+
+@shop_bp.route('/<int:book_id>', methods=['DELETE'])
+def delete_book(book_id):
+    """Delete a book from the inventory."""
+    try:
+        # Check if book exists
+        existing_book = database.get_article_by_id(book_id)
+        if not existing_book:
+            return jsonify({"error": f"Book with ID {book_id} not found"}), 404
+
+        # Delete the book
+        database.delete_article(book_id)
+
+        return jsonify({"message": f"Book with ID {book_id} deleted successfully"}), 200
+
+    except Exception as e:
+        logger.error(f"Error deleting book {book_id}: {str(e)}")
+        return jsonify({"error": f"Failed to delete book {book_id}"}), 500
+```
+
+{{< /tab >}}
+
+{{< tab title="app/database.py" >}}
+
+Database operations with connection management.
+
+```python
+import sqlite3
+import os
+import logging
+from contextlib import contextmanager
+from app.models.article import Article
+
+# Set up logging
+logger = logging.getLogger(__name__)
+
+# Get database path from environment variable or use default
+DATABASE_PATH = os.environ.get('DATABASE_PATH', 'bookstore.db')
+
+@contextmanager
+def get_db_connection():
+    """
+    Context manager for database connections.
+    Automatically handles connection opening, committing, and closing.
+
+    Yields:
+        sqlite3.Connection: Database connection object
+    """
+    conn = None
+    try:
+        conn = sqlite3.connect(DATABASE_PATH)
+        # Configure connection to return rows as dictionaries
+        conn.row_factory = sqlite3.Row
+        yield conn
+        conn.commit()
+    except sqlite3.Error as e:
+        if conn:
+            conn.rollback()
+        logger.error(f"Database error: {str(e)}")
+        raise
+    finally:
+        if conn:
+            conn.close()
+
+def initialize_database():
+    """
+    Initialize the database by creating the articles table if it doesn't exist.
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            # Create articles table
+            cursor.execute('''
+                CREATE TABLE IF NOT EXISTS articles (
+                    id INTEGER PRIMARY KEY AUTOINCREMENT,
+                    name TEXT NOT NULL,
+                    price REAL NOT NULL,
+                    quantity INTEGER NOT NULL
+                )
+            ''')
+
+            logger.info("Database initialized successfully")
+    except sqlite3.Error as e:
+        logger.error(f"Failed to initialize database: {str(e)}")
+        raise
+
+def add_article(article):
+    """
+    Add a new article to the database.
+
+    Args:
+        article (Article): Article object to add
+
+    Returns:
+        int: ID of the newly added article
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute(
+                "INSERT INTO articles (name, price, quantity) VALUES (?, ?, ?)",
+                (article.name, article.price, article.quantity)
+            )
+
+            # Get the ID of the newly inserted article
+            article_id = cursor.lastrowid
+            logger.info(f"Added article with ID {article_id}")
+            return article_id
+    except sqlite3.Error as e:
+        logger.error(f"Failed to add article: {str(e)}")
+        raise
+
+def get_article_by_id(article_id):
+    """
+    Get an article by its ID.
+
+    Args:
+        article_id (int): ID of the article to retrieve
+
+    Returns:
+        Article: Article object if found, None otherwise
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("SELECT * FROM articles WHERE id = ?", (article_id,))
+            row = cursor.fetchone()
+
+            if row:
+                return Article(
+                    name=row['name'],
+                    price=row['price'],
+                    quantity=row['quantity'],
+                    article_id=row['id']
+                )
+            return None
+    except sqlite3.Error as e:
+        logger.error(f"Failed to get article {article_id}: {str(e)}")
+        raise
+
+def get_all_articles():
+    """
+    Get all articles from the database.
+
+    Returns:
+        list: List of Article objects
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("SELECT * FROM articles")
+            rows = cursor.fetchall()
+
+            articles = []
+            for row in rows:
+                article = Article(
+                    name=row['name'],
+                    price=row['price'],
+                    quantity=row['quantity'],
+                    article_id=row['id']
+                )
+                articles.append(article)
+
+            return articles
+    except sqlite3.Error as e:
+        logger.error(f"Failed to get all articles: {str(e)}")
+        raise
+
+def update_article(article):
+    """
+    Update an existing article in the database.
+
+    Args:
+        article (Article): Article object with updated values
+
+    Returns:
+        bool: True if successful, False if article not found
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute(
+                "UPDATE articles SET name = ?, price = ?, quantity = ? WHERE id = ?",
+                (article.name, article.price, article.quantity, article.id)
+            )
+
+            # Check if an article was actually updated
+            if cursor.rowcount == 0:
+                logger.warning(f"No article found with ID {article.id}")
+                return False
+
+            logger.info(f"Updated article with ID {article.id}")
+            return True
+    except sqlite3.Error as e:
+        logger.error(f"Failed to update article {article.id}: {str(e)}")
+        raise
+
+def delete_article(article_id):
+    """
+    Delete an article from the database.
+
+    Args:
+        article_id (int): ID of the article to delete
+
+    Returns:
+        bool: True if successful, False if article not found
+    """
+    try:
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+
+            cursor.execute("DELETE FROM articles WHERE id = ?", (article_id,))
+
+            # Check if an article was actually deleted
+            if cursor.rowcount == 0:
+                logger.warning(f"No article found with ID {article_id}")
+                return False
+
+            logger.info(f"Deleted article with ID {article_id}")
+            return True
+    except sqlite3.Error as e:
+        logger.error(f"Failed to delete article {article_id}: {str(e)}")
+        raise
+```
+
+{{< /tab >}}
+<!-- markdownlint-disable -->
+{{< tab title="app/__init__.py" >}}
+<!-- markdownlint-enable -->
+Flask application factory.
+
+```python
+import os
+from flask import Flask
+from dotenv import load_dotenv
+
+def create_app(test_config=None):
+    """
+    Application factory for creating the Flask app.
+
+    Args:
+        test_config (dict, optional): Test configuration to override default config
+
+    Returns:
+        Flask: Configured Flask application
+    """
+    # Load environment variables from .env file
+    load_dotenv()
+
+    # Create and configure the app
+    app = Flask(__name__, instance_relative_config=True)
+
+    # Set default configuration
+    app.config.from_mapping(
+        SECRET_KEY=os.environ.get('SECRET_KEY', 'dev'),
+        DATABASE_PATH=os.environ.get('DATABASE_URL', 'bookstore.db'),
+        BOOK_TITLE_MAX_LENGTH=int(os.environ.get('BOOK_TITLE_MAX_LENGTH', 100)),
+        MAX_PRICE=float(os.environ.get('MAX_PRICE', 1000.00)),
+        MAX_QUANTITY=int(os.environ.get('MAX_QUANTITY', 1000))
+    )
+
+    # Override config with test config if provided
+    if test_config:
+        app.config.update(test_config)
+
+    # Ensure the instance folder exists
+    os.makedirs(app.instance_path, exist_ok=True)
+
+    # Initialize database
+    from app import database
+    database.initialize_database()
+
+    # Register blueprints
+    from app.routes.shop import shop_bp
+    app.register_blueprint(shop_bp)
+
+    # Add a simple index route
+    @app.route('/')
+    def index():
+        return {
+            "message": "Welcome to the Bookstore Inventory API",
+            "endpoints": {
+                "books": "/books",
+                "book_by_id": "/books/<id>"
+            }
+        }
+
+    return app
+```
+
+{{< /tab >}}
+
+{{< /tabs >}}
+
+1. Check your code files against these examples.
+
+1. To verify if your code works, ask Chat how to start a local application server:
+
+   ```plaintext
+   How do I start a local application server for my Python web application?
+   ```
+
+1. Follow the instructions, and check if your application is working.
+
+If your application is working, congratulations! You have successfully used
+GitLab Duo Chat and Code Suggestions to build working online shop application.
+
+If it is not working, then you need to find out why. Chat and Code Suggestions
+can help you create tests to ensure your application works as expected and
+identify any issues that need to be fixed.
+[Issue 1284](https://gitlab.com/gitlab-org/technical-writing/team-tasks/-/issues/1284)
+exists to create this tutorial.
+
+## Related topics
+
+- [GitLab Duo use cases](../use_cases.md)
+- [Get started with GitLab Duo](../../get_started/getting_started_gitlab_duo.md).
+- Blog post: [Streamline DevSecOps engineering workflows with GitLab Duo](https://about.gitlab.com/blog/2024/12/05/streamline-devsecops-engineering-workflows-with-gitlab-duo/)
+<!-- markdownlint-disable -->
+- <i class="fa-youtube-play" aria-hidden="true"></i>
+  [GitLab Duo Chat](https://youtu.be/ZQBAuf-CTAY?si=0o9-xJ_ATTsL1oew)
+<!-- Video published on 2024-04-18 -->
+- <i class="fa-youtube-play" aria-hidden="true"></i>
+  [GitLab Duo Code Suggestions](https://youtu.be/ds7SG1wgcVM?si=MfbzPIDpikGhoPh7)
+<!-- Video published on 2024-01-24 -->
+<!-- markdownlint-enable -->
diff --git a/doc/user/group/saml_sso/example_saml_config.md b/doc/user/group/saml_sso/example_saml_config.md
index f775eca24a8dce56b9bae5ef6f821efd39d4a811..2a812da0a370ca60a1bd98bc07d03e41a7453d05 100644
--- a/doc/user/group/saml_sso/example_saml_config.md
+++ b/doc/user/group/saml_sso/example_saml_config.md
@@ -134,19 +134,19 @@ Setting the username for the newly provisioned users when assigning them the SCI
 
 ### Basic SAML app configuration
 
-![OneLogin application details](img/OneLogin-app_details_v12.png)
+![OneLogin application details](img/OneLogin-app_details_v12_8.png)
 
 ### Parameters
 
-![OneLogin application details](img/OneLogin-parameters_v12.png)
+![OneLogin application details](img/OneLogin-parameters_v12_8.png)
 
 ### Adding a user
 
-![OneLogin user add](img/OneLogin-userAdd_v12.png)
+![OneLogin user add](img/OneLogin-userAdd_v12_8.png)
 
 ### SSO settings
 
-![OneLogin SSO settings](img/OneLogin-SSOsettings_v12.png)
+![OneLogin SSO settings](img/OneLogin-SSOsettings_v12_8.png)
 
 ## SAML response example
 
diff --git a/doc/user/group/saml_sso/img/OneLogin-SSOsettings_v12.png b/doc/user/group/saml_sso/img/OneLogin-SSOsettings_v12_8.png
similarity index 100%
rename from doc/user/group/saml_sso/img/OneLogin-SSOsettings_v12.png
rename to doc/user/group/saml_sso/img/OneLogin-SSOsettings_v12_8.png
diff --git a/doc/user/group/saml_sso/img/OneLogin-app_details_v12.png b/doc/user/group/saml_sso/img/OneLogin-app_details_v12_8.png
similarity index 100%
rename from doc/user/group/saml_sso/img/OneLogin-app_details_v12.png
rename to doc/user/group/saml_sso/img/OneLogin-app_details_v12_8.png
diff --git a/doc/user/group/saml_sso/img/OneLogin-parameters_v12.png b/doc/user/group/saml_sso/img/OneLogin-parameters_v12_8.png
similarity index 100%
rename from doc/user/group/saml_sso/img/OneLogin-parameters_v12.png
rename to doc/user/group/saml_sso/img/OneLogin-parameters_v12_8.png
diff --git a/doc/user/group/saml_sso/img/OneLogin-userAdd_v12.png b/doc/user/group/saml_sso/img/OneLogin-userAdd_v12_8.png
similarity index 100%
rename from doc/user/group/saml_sso/img/OneLogin-userAdd_v12.png
rename to doc/user/group/saml_sso/img/OneLogin-userAdd_v12_8.png
diff --git a/doc/user/project/repository/code_suggestions/_index.md b/doc/user/project/repository/code_suggestions/_index.md
index 2073e0caaf0ed1a8ad3e52fe359f4e10d0d26a39..8e4fc23edd2fd76bc77d83c1384cd710540e5e40 100644
--- a/doc/user/project/repository/code_suggestions/_index.md
+++ b/doc/user/project/repository/code_suggestions/_index.md
@@ -251,6 +251,23 @@ that explain what you want to build:
 - Code generation treats your code comments like chat.
 - Your code comments update the `user_instruction`, and then improve the next results you receive.
 
+### Using imported files as context
+
+{{< history >}}
+
+- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/514124) in GitLab 17.9 [with a flag](../../../../administration/feature_flags.md) named `code_suggestions_include_context_imports`. Disabled by default.
+
+{{< /history >}}
+
+{{< alert type="flag" >}}
+
+The availability of this feature is controlled by a feature flag.
+For more information, see the history.
+
+{{< /alert >}}
+
+Use the imported files in your IDE to provide context about your code project. Imported file context is supported for JavaScript and TypeScript files, including [`js`], [`jsx`], [`ts`], [`tsx`], and [`vue`] file types.
+
 ## Truncation of file content
 
 Because of LLM limits and performance reasons, the content of the currently
diff --git a/ee/app/assets/javascripts/dependencies/components/dependency_export_dropdown.vue b/ee/app/assets/javascripts/dependencies/components/dependency_export_dropdown.vue
index 80e1620b4ac934a0566012c1213e5ab507a5d750..3c4c886c83bf1a02ebbe642846841534dfbf6d01 100644
--- a/ee/app/assets/javascripts/dependencies/components/dependency_export_dropdown.vue
+++ b/ee/app/assets/javascripts/dependencies/components/dependency_export_dropdown.vue
@@ -8,7 +8,6 @@ import {
 // eslint-disable-next-line no-restricted-imports
 import { mapActions, mapState } from 'vuex';
 import { s__ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
 import {
   EXPORT_FORMAT_CSV,
   EXPORT_FORMAT_DEPENDENCY_LIST,
@@ -39,13 +38,7 @@ const exportFormats = [
     type: EXPORT_FORMAT_CSV,
     buttonText: s__('Dependencies|Export as CSV'),
     testid: 'csv-item',
-    available: (component) => {
-      if (component.container === NAMESPACE_GROUP) {
-        return component.glFeatures.groupDependencyListCsvExport;
-      }
-
-      return availableForContainers([NAMESPACE_PROJECT, NAMESPACE_ORGANIZATION])(component);
-    },
+    available: availableForContainers([NAMESPACE_PROJECT, NAMESPACE_GROUP, NAMESPACE_ORGANIZATION]),
   },
 ];
 
@@ -59,7 +52,6 @@ export default {
   directives: {
     GlTooltip: GlTooltipDirective,
   },
-  mixins: [glFeatureFlagMixin()],
   props: {
     // Used in availability check.
     // eslint-disable-next-line vue/no-unused-properties
diff --git a/ee/app/controllers/groups/dependencies_controller.rb b/ee/app/controllers/groups/dependencies_controller.rb
index d3fd9059e505a3bc86cebd16bcb5e54d9da2a3c0..c96c7a512a1ac9a8a12c3b6fd2ad8b45177d10f6 100644
--- a/ee/app/controllers/groups/dependencies_controller.rb
+++ b/ee/app/controllers/groups/dependencies_controller.rb
@@ -9,7 +9,6 @@ class DependenciesController < Groups::ApplicationController
       push_frontend_feature_flag(:group_level_dependencies_filtering_by_packager, group)
       push_frontend_feature_flag(:version_filtering_on_group_level_dependency_list, group)
       push_frontend_feature_flag(:dependency_paths, group)
-      push_frontend_feature_flag(:group_dependency_list_csv_export, group)
     end
 
     before_action :authorize_read_dependency_list!
diff --git a/ee/app/helpers/ee/preferences_helper.rb b/ee/app/helpers/ee/preferences_helper.rb
index de40ade83ef3a7f7b65e5ad56cea43d4b25ad86b..cb4b9fd83d2b45738beaeb47f8e93ac7d7cdcc3c 100644
--- a/ee/app/helpers/ee/preferences_helper.rb
+++ b/ee/app/helpers/ee/preferences_helper.rb
@@ -15,6 +15,8 @@ def excluded_dashboard_choices
     override :extensions_marketplace_view
 
     def extensions_marketplace_view
+      return unless ::WebIde::ExtensionMarketplace.feature_enabled_from_application_settings?(user: current_user)
+
       if License.feature_available?(:remote_development) &&
           ::WebIde::ExtensionMarketplace.feature_enabled?(user: current_user)
         build_extensions_marketplace_view(
diff --git a/ee/config/feature_flags/gitlab_com_derisk/group_dependency_list_csv_export.yml b/ee/config/feature_flags/gitlab_com_derisk/group_dependency_list_csv_export.yml
deleted file mode 100644
index 538b15165a54fc17d20f2d654040e1e9364c24e1..0000000000000000000000000000000000000000
--- a/ee/config/feature_flags/gitlab_com_derisk/group_dependency_list_csv_export.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: group_dependency_list_csv_export
-feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/435843
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183097
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/523239
-milestone: '17.10'
-group: group::security insights
-type: gitlab_com_derisk
-default_enabled: false
diff --git a/ee/spec/factories/remote_development/workspaces.rb b/ee/spec/factories/remote_development/workspaces.rb
index 30a8f19d269c46b3cdeea3c74a511cbde85fee14..c3695013d18bb27663a184138e3e99c6f5209f88 100644
--- a/ee/spec/factories/remote_development/workspaces.rb
+++ b/ee/spec/factories/remote_development/workspaces.rb
@@ -84,8 +84,7 @@
             user_name: workspace.user.name,
             user_email: workspace.user.email,
             workspace_id: workspace.id,
-            vscode_extension_marketplace:
-              WebIde::Settings::DefaultSettings.default_settings.fetch(:vscode_extension_marketplace).first,
+            vscode_extension_marketplace: ::WebIde::ExtensionMarketplacePreset.open_vsx.values,
             variables: []
           )
 
diff --git a/ee/spec/frontend/dependencies/components/dependency_export_dropdown_spec.js b/ee/spec/frontend/dependencies/components/dependency_export_dropdown_spec.js
index 0d475962621ad1d1d38f0617f4d8b22dfc38e64f..7edd8db981a6001fa9023b199a7a5ec6594b80cc 100644
--- a/ee/spec/frontend/dependencies/components/dependency_export_dropdown_spec.js
+++ b/ee/spec/frontend/dependencies/components/dependency_export_dropdown_spec.js
@@ -24,11 +24,8 @@ describe('DependencyExportDropdown component', () => {
 
     wrapper = shallowMountExtended(DependencyExportDropdown, {
       store,
+      provide,
       propsData: props,
-      provide: {
-        glFeatures: { groupDependencyListCsvExport: true },
-        ...provide,
-      },
     });
   };
 
@@ -103,27 +100,6 @@ describe('DependencyExportDropdown component', () => {
 
     itHasCorrectLoadingLogic(() => findDisclosure());
     itShowsDisclosureWithItems([jsonArrayItem, csvItem]);
-
-    describe('when group CSV export feature flag is disabled', () => {
-      beforeEach(() => {
-        factory({
-          props: { container: NAMESPACE_GROUP },
-          provide: { glFeatures: { groupDependencyListCsvExport: false } },
-        });
-      });
-
-      it('shows button that dispatches JSON export', () => {
-        const button = findButton();
-
-        expect(button.exists()).toBe(true);
-
-        button.vm.$emit('click');
-
-        expect(store.dispatch).toHaveBeenCalledWith(`${allNamespace}/fetchExport`, {
-          export_type: EXPORT_FORMAT_JSON_ARRAY,
-        });
-      });
-    });
   });
 
   describe('when container is an organization', () => {
diff --git a/ee/spec/helpers/preferences_helper_spec.rb b/ee/spec/helpers/preferences_helper_spec.rb
index a1ce1fc2d202104338263d066b5c1fad2249604e..4088e2d3774ec42d1f8e28aebabe301cc6f6f76f 100644
--- a/ee/spec/helpers/preferences_helper_spec.rb
+++ b/ee/spec/helpers/preferences_helper_spec.rb
@@ -48,17 +48,34 @@
   end
 
   describe '#extensions_marketplace_view' do
+    let(:feature_enabled) { false }
+    let(:application_setting_enabled) { true }
+
     subject { helper.extensions_marketplace_view }
 
+    before do
+      allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled?)
+        .with(user: user)
+        .and_return(feature_enabled)
+
+      allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled_from_application_settings?)
+        .with(user: user)
+        .and_return(application_setting_enabled)
+    end
+
+    context 'when feature not enabled at application_settings' do
+      let(:application_setting_enabled) { false }
+
+      it { is_expected.to be_nil }
+    end
+
     context 'when remote_development licensed feature is enabled' do
       before do
         stub_licensed_features(remote_development: true)
       end
 
       context 'when Web IDE Extension Marketplace feature is enabled' do
-        before do
-          allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled?).with(user: user).and_return(true)
-        end
+        let(:feature_enabled) { true }
 
         it { is_expected.to match(a_hash_including(title: 'Web IDE and Workspaces', message: /IDE and Workspaces/)) }
       end
@@ -70,9 +87,7 @@
 
     context 'when remote_development licensed feature is not enabled' do
       context 'when Web IDE Extension Marketplace feature is enabled' do
-        before do
-          allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled?).with(user: user).and_return(true)
-        end
+        let(:feature_enabled) { true }
 
         it { is_expected.to match(a_hash_including(title: 'Web IDE', message: /for the Web IDE/)) }
       end
diff --git a/ee/spec/lib/ee/web_ide/settings/extension_marketplace_metadata_generator_spec.rb b/ee/spec/lib/ee/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
index 24be6d450b12a1feebf5b5082cddbbe8227e6957..46fa65abcae089c81e2bd4e06065b351745d26c5 100644
--- a/ee/spec/lib/ee/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
+++ b/ee/spec/lib/ee/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
@@ -5,7 +5,17 @@
 RSpec.describe WebIde::Settings::ExtensionMarketplaceMetadataGenerator, feature_category: :web_ide do
   using RSpec::Parameterized::TableSyntax
 
-  let(:user_class) { stub_const('User', Class.new) }
+  let(:user_class) do
+    stub_const(
+      "User",
+      Class.new do
+        def flipper_id
+          "UserStub"
+        end
+      end
+    )
+  end
+
   let(:group_class) { stub_const('Namespace', Class.new) }
   let(:user) { user_class.new }
   let(:group) { group_class.new }
@@ -43,6 +53,10 @@
         extensions_marketplace_opt_in_status: :unset
       )
       allow(group).to receive(:enterprise_users_extensions_marketplace_enabled?).and_return(enterprise_group_enabled)
+
+      allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled_from_application_settings?)
+        .with(user: user)
+        .and_return(true)
     end
 
     it "adds settings with disabled reason based on enterprise_group presence and setting" do
diff --git a/ee/spec/lib/web_ide/settings/settings_integration_spec.rb b/ee/spec/lib/web_ide/settings/settings_integration_spec.rb
index 2fa6a9e107d1f6b8fae7d07afdf1f45b3b364ead..72219f8b1a12bc91d2cd324bd8625a5da37e1b01 100644
--- a/ee/spec/lib/web_ide/settings/settings_integration_spec.rb
+++ b/ee/spec/lib/web_ide/settings/settings_integration_spec.rb
@@ -15,8 +15,13 @@
   subject(:settings) { described_class.get([:vscode_extension_marketplace_metadata], options) }
 
   before do
-    stub_feature_flags(vscode_web_ide: true, web_ide_extensions_marketplace: true)
+    stub_feature_flags(
+      vscode_web_ide: true,
+      web_ide_extensions_marketplace: true,
+      vscode_extension_marketplace_settings: true
+    )
     stub_licensed_features(disable_extensions_marketplace_for_enterprise_users: true)
+    stub_application_setting(vscode_extension_marketplace: { enabled: true, preset: 'open_vsx' })
     user.update!(extensions_marketplace_enabled: true)
   end
 
diff --git a/ee/spec/requests/groups/dependencies_controller_spec.rb b/ee/spec/requests/groups/dependencies_controller_spec.rb
index 9795af9b219eb3d37def1fdaaece1ebf1aefa5c0..c57a8f1bcb9aad4cd86397a40306c8e2210c2505 100644
--- a/ee/spec/requests/groups/dependencies_controller_spec.rb
+++ b/ee/spec/requests/groups/dependencies_controller_spec.rb
@@ -30,12 +30,6 @@
             expect(response).to have_gitlab_http_status(:ok)
           end
 
-          it 'pushes group dependency list CSV export feature flag' do
-            subject
-
-            expect(response.body).to have_pushed_frontend_feature_flags(groupDependencyListCsvExport: true)
-          end
-
           it 'returns the correct template' do
             subject
 
diff --git a/ee/spec/requests/groups_controller_spec.rb b/ee/spec/requests/groups_controller_spec.rb
index 7f9b664a5faf075c77e9284c41efe2dcc1a65202..a019df293ab85f2ba11fca00e66fb0b293fbd15a 100644
--- a/ee/spec/requests/groups_controller_spec.rb
+++ b/ee/spec/requests/groups_controller_spec.rb
@@ -452,6 +452,7 @@
           vscode_web_ide: true,
           web_ide_extensions_marketplace: true
         )
+        stub_application_setting(vscode_extension_marketplace: { enabled: true, preset: 'open_vsx' })
       end
 
       it 'does not change the column' do
diff --git a/lib/web_ide/extension_marketplace.rb b/lib/web_ide/extension_marketplace.rb
index 61cd2c63386f89d9e63c5cf70d607ad7782cbe1a..a2da3bb37316548917343359b0b095ac98e85ce6 100644
--- a/lib/web_ide/extension_marketplace.rb
+++ b/lib/web_ide/extension_marketplace.rb
@@ -2,28 +2,43 @@
 
 module WebIde
   module ExtensionMarketplace
-    # This returns true if the extensions marketplace feature is available to any users
+    # Returns true if the extensions marketplace feature is enabled for any users
     #
     # @return [Boolean]
     def self.feature_enabled_for_any_user?
-      feature_flag_enabled_for_any_actor?(:web_ide_extensions_marketplace) &&
+      # note: Intentionally pass `nil` here since we don't have a user in scope
+      feature_enabled_from_application_settings?(user: nil) &&
+        feature_flag_enabled_for_any_actor?(:web_ide_extensions_marketplace) &&
         feature_flag_enabled_for_any_actor?(:vscode_web_ide)
     end
 
-    # This returns true if the extensions marketplace feature is available to the given user
+    # Returns true if the extensions marketplace feature is enabled for the given user
     #
     # @param user [User]
     # @return [Boolean]
     def self.feature_enabled?(user:)
-      Feature.enabled?(:web_ide_extensions_marketplace, user) &&
-        Feature.enabled?(:vscode_web_ide, user)
+      feature_enabled_from_application_settings?(user: user) &&
+        feature_enabled_from_flags?(user: user)
+    end
+
+    # Returns true if the ExtensionMarketplace feature is enabled from application settings
+    #
+    # @param user [User, nil] Current user for feature enablement context
+    # @return [Boolean]
+    def self.feature_enabled_from_application_settings?(user:)
+      return true unless should_use_application_settings?(user: user)
+
+      Gitlab::CurrentSettings.vscode_extension_marketplace&.fetch('enabled', false)
     end
 
     # This value is used when the end-user is accepting the third-party extension marketplace integration.
     #
+    # @param user [User] Current user for context
     # @return [String] URL of the VSCode Extension Marketplace home
-    def self.marketplace_home_url
-      "https://open-vsx.org"
+    def self.marketplace_home_url(user:)
+      Gitlab::SafeRequestStore.fetch(:vscode_extension_marketplace_home_url) do
+        Settings.get_single_setting(:vscode_extension_marketplace_home_url, user: user)
+      end
     end
 
     # @return [String] URL of the help page for the user preferences for Extensions Marketplace opt-in
@@ -43,7 +58,7 @@ def self.webide_extension_marketplace_settings(user:)
       Settings.get_single_setting(
         :vscode_extension_marketplace_view_model,
         user: user,
-        vscode_extension_marketplace_feature_flag_enabled: feature_enabled?(user: user)
+        vscode_extension_marketplace_feature_flag_enabled: feature_enabled_from_flags?(user: user)
       )
     end
 
@@ -61,6 +76,28 @@ def self.feature_flag_enabled_for_any_actor?(flag)
       feature && !feature.off?
     end
 
-    private_class_method :feature_flag_enabled_for_any_actor?
+    # Returns true if we should use `feature_enabled_from_application_settings?` to determine feature availability
+    #
+    # @param user [User, nil] Current user for feature enablement context
+    # @return [Boolean]
+    def self.should_use_application_settings?(user:)
+      if user
+        Feature.enabled?(:vscode_extension_marketplace_settings, user)
+      else
+        feature_flag_enabled_for_any_actor?(:vscode_extension_marketplace_settings)
+      end
+    end
+
+    # This returns true if the extensions marketplace flags are enabled
+    #
+    # @param user [User]
+    # @return [Boolean]
+    def self.feature_enabled_from_flags?(user:)
+      Feature.enabled?(:web_ide_extensions_marketplace, user) &&
+        Feature.enabled?(:vscode_web_ide, user)
+    end
+
+    private_class_method :feature_flag_enabled_for_any_actor?, :should_use_application_settings?,
+      :feature_enabled_from_flags?
   end
 end
diff --git a/lib/web_ide/extension_marketplace_preset.rb b/lib/web_ide/extension_marketplace_preset.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a42371669a61012018f161d2455992231e17006
--- /dev/null
+++ b/lib/web_ide/extension_marketplace_preset.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module WebIde
+  class ExtensionMarketplacePreset
+    CUSTOM_KEY = "custom"
+
+    def self.all
+      [open_vsx]
+    end
+
+    def self.open_vsx
+      # note: This is effectively a constant so lets memoize
+      @open_vsx ||= new(
+        "open_vsx",
+        "Open VSX",
+        # See https://open-vsx.org/swagger-ui/index.html?urls.primaryName=VSCode%20Adapter for OpenVSX Swagger API
+        service_url: "https://open-vsx.org/vscode/gallery",
+        item_url: "https://open-vsx.org/vscode/item",
+        resource_url_template: "https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}"
+      )
+    end
+
+    def initialize(key, name, service_url:, item_url:, resource_url_template:)
+      @key = key
+      @name = name
+      @values = {
+        service_url: service_url,
+        item_url: item_url,
+        resource_url_template: resource_url_template
+      }.freeze
+    end
+
+    attr_reader :key, :name, :values
+
+    def to_h
+      {
+        key: key,
+        name: name,
+        values: values
+      }
+    end
+  end
+end
diff --git a/lib/web_ide/settings/default_settings.rb b/lib/web_ide/settings/default_settings.rb
index 19df3da3b065c27117a0704bed0af2b4f4b8def5..20a690034d4236ebae91ba4b71b9edee2e618934 100644
--- a/lib/web_ide/settings/default_settings.rb
+++ b/lib/web_ide/settings/default_settings.rb
@@ -8,24 +8,17 @@ class DefaultSettings
       def self.default_settings
         {
           vscode_extension_marketplace: [
-            # See https://sourcegraph.com/github.com/microsoft/vscode@6979fb003bfa575848eda2d3966e872a9615427b/-/blob/src/vs/base/common/product.ts?L96
-            #     for the original source of settings entries in the VS Code source code.
-            # See https://open-vsx.org/swagger-ui/index.html?urls.primaryName=VSCode%20Adapter#
-            #     for OpenVSX Swagger API
-            {
-              service_url: "https://open-vsx.org/vscode/gallery",
-              item_url: "https://open-vsx.org/vscode/item",
-              resource_url_template: "https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}",
-              control_url: "",
-              nls_base_url: "",
-              publisher_url: ""
-            },
+            {}, # NOTE: There is no default, the value is always generated by ExtensionMarketplaceGenerator
             Hash
           ],
           vscode_extension_marketplace_metadata: [
             { enabled: false, disabled_reason: :instance_disabled },
             Hash
           ],
+          vscode_extension_marketplace_home_url: [
+            "",
+            String
+          ],
           vscode_extension_marketplace_view_model: [
             { enabled: false, reason: :instance_disabled, help_url: '' },
             Hash
diff --git a/lib/web_ide/settings/extension_marketplace_generator.rb b/lib/web_ide/settings/extension_marketplace_generator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..47de4e9a054688c3a08c214082b05fca2d80d6fc
--- /dev/null
+++ b/lib/web_ide/settings/extension_marketplace_generator.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module WebIde
+  module Settings
+    class ExtensionMarketplaceGenerator
+      # @param [Hash] context
+      # @return [Hash]
+      def self.generate(context)
+        return context unless context.fetch(:requested_setting_names).include?(:vscode_extension_marketplace)
+
+        user = context.dig(:options, :user)
+
+        context[:settings][:vscode_extension_marketplace] = extension_marketplace_from_application_settings(user)
+        context
+      end
+
+      # @param [User, nil] user
+      # @return [Hash]
+      def self.extension_marketplace_from_application_settings(user)
+        unless Feature.enabled?(:vscode_extension_marketplace_settings, user)
+          return ::WebIde::ExtensionMarketplacePreset.open_vsx.values
+        end
+
+        settings = Gitlab::CurrentSettings.vscode_extension_marketplace
+        preset_key = settings.fetch("preset", ::WebIde::ExtensionMarketplacePreset.open_vsx.key)
+
+        if preset_key == ::WebIde::ExtensionMarketplacePreset::CUSTOM_KEY
+          settings.fetch("custom_values").deep_symbolize_keys
+        else
+          preset = ::WebIde::ExtensionMarketplacePreset.all.find { |x| x.key == preset_key }
+          preset&.values
+        end
+      end
+
+      private_class_method :extension_marketplace_from_application_settings
+    end
+  end
+end
diff --git a/lib/web_ide/settings/extension_marketplace_home_url_generator.rb b/lib/web_ide/settings/extension_marketplace_home_url_generator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d691e0477fd07df4f5e5e56af24299ab4e42d981
--- /dev/null
+++ b/lib/web_ide/settings/extension_marketplace_home_url_generator.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module WebIde
+  module Settings
+    class ExtensionMarketplaceHomeUrlGenerator
+      # @param [Hash] context
+      # @return [Hash]
+      def self.generate(context)
+        return context unless context.fetch(:requested_setting_names).include?(:vscode_extension_marketplace_home_url)
+
+        context[:settings][:vscode_extension_marketplace_home_url] = home_url(context)
+        context
+      end
+
+      # @param [Hash] context
+      # @return [String] The URL to use for the extension marketplace home
+      def self.home_url(context)
+        context => {
+          settings: {
+            vscode_extension_marketplace: Hash => vscode_settings,
+          }
+        }
+
+        item_url = vscode_settings&.fetch(:item_url, nil)
+
+        return "" unless item_url
+
+        base_url = ::Gitlab::UrlHelpers.normalized_base_url(item_url)
+
+        # NOTE: It's possible for `normalized_base_url` to return something like `://` so let's go ahead and check
+        #       that we actually start with `http` or `https`.
+        return base_url if /^https?:/.match?(base_url)
+
+        ""
+      end
+
+      private_class_method :home_url
+    end
+  end
+end
diff --git a/lib/web_ide/settings/extension_marketplace_metadata_generator.rb b/lib/web_ide/settings/extension_marketplace_metadata_generator.rb
index af71de6b0c88a0e61ac61f8a8a0cb47442dea1aa..40fe374c4129f70303868fe58b7afc5bd671e273 100644
--- a/lib/web_ide/settings/extension_marketplace_metadata_generator.rb
+++ b/lib/web_ide/settings/extension_marketplace_metadata_generator.rb
@@ -48,6 +48,10 @@ def self.build_metadata(user:, flag_enabled:)
         return metadata_disabled(:no_flag) if flag_enabled.nil?
         return metadata_disabled(:instance_disabled) unless flag_enabled
 
+        unless ::WebIde::ExtensionMarketplace.feature_enabled_from_application_settings?(user: user)
+          return metadata_disabled(:instance_disabled)
+        end
+
         build_metadata_for_user(user)
       end
 
diff --git a/lib/web_ide/settings/main.rb b/lib/web_ide/settings/main.rb
index 49fb2656f804a89a3175c1184864fe6906deda34..e845fd02ecbcb4e22e2011d5dde75621fe6bf365 100644
--- a/lib/web_ide/settings/main.rb
+++ b/lib/web_ide/settings/main.rb
@@ -17,6 +17,8 @@ def self.get_settings(context)
         result =
           initial_result
             .map(SettingsInitializer.method(:init))
+            .map(ExtensionMarketplaceGenerator.method(:generate))
+            .map(ExtensionMarketplaceHomeUrlGenerator.method(:generate))
             .map(ExtensionMarketplaceMetadataGenerator.method(:generate))
             # NOTE: EnvVarOverrideProcessor is inserted here to easily override settings for local or temporary testing
             #       it should happen **before** validators.
diff --git a/lib/web_ide/settings/settings_initializer.rb b/lib/web_ide/settings/settings_initializer.rb
index ec29038f4aac94420cc903d6899b16b9c5f75f5a..26ae8b2e63fe04da936a18b35b4ff20d6c5aa6be 100644
--- a/lib/web_ide/settings/settings_initializer.rb
+++ b/lib/web_ide/settings/settings_initializer.rb
@@ -7,6 +7,9 @@ class SettingsInitializer
         vscode_extension_marketplace_view_model: [
           :vscode_extension_marketplace_metadata,
           :vscode_extension_marketplace
+        ],
+        vscode_extension_marketplace_home_url: [
+          :vscode_extension_marketplace
         ]
       }.freeze
 
diff --git a/package.json b/package.json
index ccfc5b2260a4a7a861e93f87c512797ddd057e6d..2f74fe391e4761cb6d40e6a6d3128fe79ea68846 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
     "@gitlab/duo-ui": "^8.4.0",
     "@gitlab/favicon-overlay": "2.0.0",
     "@gitlab/fonts": "^1.3.0",
-    "@gitlab/query-language-rust": "0.4.0",
+    "@gitlab/query-language-rust": "0.4.2",
     "@gitlab/svgs": "3.123.0",
     "@gitlab/ui": "110.1.0",
     "@gitlab/vue-router-vue3": "npm:vue-router@4.5.0",
diff --git a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
index 7893a75b5b4228ff11f5f5c9348fc0dd1ae38081..485939f4e44eae079709cc1f71bfa0762dcf6a80 100644
--- a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_spec.js
@@ -2,8 +2,13 @@ import { GlForm, GlCollapsibleListbox, GlLoadingIcon } from '@gitlab/ui';
 import { nextTick } from 'vue';
 import { mountExtended } from 'helpers/vue_test_utils_helper';
 import { groupIterationsResponse } from 'jest/work_items/mock_data';
+import { shouldDisableShortcuts } from '~/behaviors/shortcuts/shortcuts_toggle';
+import { keysFor } from '~/behaviors/shortcuts/keybindings';
 import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
 
+jest.mock('~/behaviors/shortcuts/shortcuts_toggle');
+jest.mock('~/behaviors/shortcuts/keybindings');
+
 describe('WorkItemSidebarDropdownWidget component', () => {
   let wrapper;
 
@@ -28,6 +33,7 @@ describe('WorkItemSidebarDropdownWidget component', () => {
     infiniteScrollLoading = false,
     clearSearchOnItemSelect = false,
     listItems = [],
+    shortcut = undefined,
   } = {}) => {
     wrapper = mountExtended(WorkItemSidebarDropdownWidget, {
       propsData: {
@@ -43,6 +49,7 @@ describe('WorkItemSidebarDropdownWidget component', () => {
         infiniteScroll,
         infiniteScrollLoading,
         clearSearchOnItemSelect,
+        shortcut,
       },
       slots,
     });
@@ -257,4 +264,47 @@ describe('WorkItemSidebarDropdownWidget component', () => {
       });
     });
   });
+  describe('shortcut tooltip', () => {
+    const shortcut = {
+      description: 'Edit dropdown',
+    };
+
+    beforeEach(() => {
+      shouldDisableShortcuts.mockReturnValue(false);
+      keysFor.mockReturnValue(['e']);
+    });
+
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    it('shows tooltip with key when shortcut is provided', () => {
+      createComponent({
+        canUpdate: true,
+        shortcut,
+      });
+      const expectedTooltip = 'Edit dropdown <kbd aria-hidden="true" class="flat gl-ml-1">e</kbd>';
+
+      expect(findEditButton().attributes('title')).toContain(expectedTooltip);
+    });
+
+    it('does not show tooltip when shortcut is not provided', () => {
+      createComponent({
+        canUpdate: true,
+      });
+
+      expect(findEditButton().attributes('title')).toBeUndefined();
+    });
+
+    it('does not show tooltip when shortcuts are disabled', () => {
+      shouldDisableShortcuts.mockReturnValue(true);
+
+      createComponent({
+        canUpdate: true,
+        shortcut,
+      });
+
+      expect(findEditButton().attributes('title')).toBeUndefined();
+    });
+  });
 });
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index aba02393fd0c6ec755027066de707335fff764d7..b74f6f810db901f329bc1428a4c8aecd6d3380d8 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -26,6 +26,7 @@ import {
   workItemResponseFactory,
 } from 'jest/work_items/mock_data';
 import { i18n, TRACKING_CATEGORY_SHOW, NEW_WORK_ITEM_IID } from '~/work_items/constants';
+import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
 
 describe('WorkItemAssignees component', () => {
   Vue.use(VueApollo);
@@ -126,6 +127,12 @@ describe('WorkItemAssignees component', () => {
     expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Assignee');
   });
 
+  it('has key shortcut tooltip', () => {
+    createComponent();
+
+    expect(findSidebarDropdownWidget().props('shortcut')).toBe(ISSUE_MR_CHANGE_ASSIGNEE);
+  });
+
   describe('Dropdown search', () => {
     it('shows no items in the dropdown when no results matching', async () => {
       createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index b393932ebfd571e685a2f8ed61a9b7a1491f5760..68801489953ff21c945ae2debd50ded1b405f1ca 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -1141,6 +1141,7 @@ describe('WorkItemDetail component', () => {
 
       it('shows the edit button', () => {
         expect(findEditButton().exists()).toBe(true);
+        expect(findEditButton().attributes('title')).toContain('Edit title and description');
       });
 
       it('renders the work item title with edit component', () => {
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 4e36531bc061d98f7a3d65e2102ea95caee57894..e389637cd500ea9b2e8fb2a6cff010801ec18c9a 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -13,6 +13,7 @@ import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutati
 import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
 import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
 import WorkItemSidebarDropdownWidget from '~/work_items/components/shared/work_item_sidebar_dropdown_widget.vue';
+import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
 import {
   projectLabelsResponse,
   groupLabelsResponse,
@@ -145,6 +146,7 @@ describe('WorkItemLabels component', () => {
       multiSelect: true,
       showFooter: true,
       itemValue: [],
+      shortcut: ISSUABLE_CHANGE_LABEL,
     });
     expect(findAllLabels()).toHaveLength(0);
   });
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index 3e361f344d80d42503f32d34f96d7964bcef03a5..00b3fd8ad2c44aba9d6399183517120db00e3c5e 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -10,6 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
 import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
 import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
 import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import { ISSUE_MR_CHANGE_MILESTONE } from '~/behaviors/shortcuts/keybindings';
 import {
   projectMilestonesResponse,
   projectMilestonesResponseWithNoMilestones,
@@ -68,6 +69,12 @@ describe('WorkItemMilestone component', () => {
     expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Milestone');
   });
 
+  it('has key shortcut tooltip', () => {
+    createComponent();
+
+    expect(findSidebarDropdownWidget().props('shortcut')).toBe(ISSUE_MR_CHANGE_MILESTONE);
+  });
+
   describe('Default text with canUpdate false and milestone value', () => {
     describe.each`
       description             | milestone                      | value
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 7c9e62c8e66cb17ac440b8e916c2dae04dbbeff8..acc24554db9c47bfb7526b2d890b997a5aea21cb 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -268,7 +268,13 @@ def stub_user(messages = {})
 
       context 'when Web IDE Extension Marketplace feature is enabled' do
         before do
-          allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled?).with(user: user).and_return(true)
+          allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled_from_application_settings?)
+            .with(user: user)
+            .and_return(true)
+
+          allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled?)
+            .with(user: user)
+            .and_return(true)
         end
 
         it 'includes extension marketplace integration' do
diff --git a/spec/lib/web_ide/extension_marketplace_preset_spec.rb b/spec/lib/web_ide/extension_marketplace_preset_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1d85b86f8213732c54bad9e6aa4a8d42dab9a8d
--- /dev/null
+++ b/spec/lib/web_ide/extension_marketplace_preset_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe WebIde::ExtensionMarketplacePreset, feature_category: :web_ide do
+  describe '.all' do
+    subject(:all) { described_class.all }
+
+    it { is_expected.to eq([described_class.open_vsx]) }
+  end
+
+  describe '.open_vsx' do
+    subject(:open_vsx) { described_class.open_vsx }
+
+    it "has OpenVSX properties" do
+      is_expected.to have_attributes(
+        key: 'open_vsx',
+        name: "Open VSX",
+        values: {
+          service_url: "https://open-vsx.org/vscode/gallery",
+          item_url: "https://open-vsx.org/vscode/item",
+          resource_url_template: "https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}"
+        }
+      )
+    end
+  end
+
+  describe '#to_h' do
+    it 'returns hash of attributes' do
+      preset = described_class.new(
+        "test_key",
+        "Test Key",
+        service_url: "abc",
+        item_url: "def",
+        resource_url_template: "ghi"
+      )
+
+      expect(preset.to_h).to eq({
+        key: "test_key",
+        name: "Test Key",
+        values: {
+          service_url: "abc",
+          item_url: "def",
+          resource_url_template: "ghi"
+        }
+      })
+    end
+  end
+end
diff --git a/spec/lib/web_ide/extension_marketplace_spec.rb b/spec/lib/web_ide/extension_marketplace_spec.rb
index 8f85e9090d99f7ec8d2c3560b3d09b07756fe9af..c3a001a8332247cb4bee66bb2f92b97d2eef8918 100644
--- a/spec/lib/web_ide/extension_marketplace_spec.rb
+++ b/spec/lib/web_ide/extension_marketplace_spec.rb
@@ -7,30 +7,41 @@
 
   let(:help_url) { "/help/user/project/web_ide/_index.md#extension-marketplace" }
   let(:user_preferences_url) { "/-/profile/preferences#integrations" }
-
-  let_it_be_with_reload(:current_user) { create(:user) }
-  let_it_be(:default_vscode_settings) do
+  let(:custom_app_setting) do
     {
-      item_url: 'https://open-vsx.org/vscode/item',
-      service_url: 'https://open-vsx.org/vscode/gallery',
-      resource_url_template:
-        'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}'
+      enabled: true,
+      preset: "custom",
+      custom_values: {
+        item_url: "https://example.com:8444/vscode/item",
+        service_url: "https://example.com:8444/vscode/service",
+        resource_url_template: "https://example.com:8444/vscode/resource"
+      }
     }
   end
 
+  let(:open_vsx_app_setting) { custom_app_setting.merge(preset: 'open_vsx') }
+
+  let_it_be_with_reload(:current_user) { create(:user) }
+
   describe 'feature enabled methods' do
-    where(:vscode_web_ide, :web_ide_extensions_marketplace, :expectation) do
-      ref(:current_user) | ref(:current_user) | true
-      ref(:current_user) | false              | false
-      false              | ref(:current_user) | false
+    where(:vscode_web_ide, :web_ide_extensions_marketplace, :vscode_extension_marketplace_settings, :app_setting,
+      :expectation) do
+      ref(:current_user) | ref(:current_user) | false | {}                | true
+      ref(:current_user) | ref(:current_user) | true  | {}                | false
+      ref(:current_user) | ref(:current_user) | true  | { enabled: true } | true
+      ref(:current_user) | false              | false | { enabled: true } | false
+      false              | ref(:current_user) | false | {}                | false
     end
 
     with_them do
       before do
         stub_feature_flags(
           vscode_web_ide: vscode_web_ide,
-          web_ide_extensions_marketplace: web_ide_extensions_marketplace
+          web_ide_extensions_marketplace: web_ide_extensions_marketplace,
+          vscode_extension_marketplace_settings: vscode_extension_marketplace_settings
         )
+
+        stub_application_setting(vscode_extension_marketplace: app_setting)
       end
 
       describe '#feature_enabled?' do
@@ -43,75 +54,89 @@
     end
   end
 
-  describe '#marketplace_home_url' do
-    it { expect(described_class.marketplace_home_url).to eq('https://open-vsx.org') }
-  end
+  describe '#feature_enabled_from_application_settings?' do
+    where(:vscode_extension_marketplace_settings, :app_setting, :user_arg, :expectation) do
+      false              | {}                 | ref(:current_user) | true
+      false              | { enabled: true }  | ref(:current_user) | true
+      false              | { enabled: true }  | nil                | true
+      ref(:current_user) | { enabled: true }  | nil                | true
+      ref(:current_user) | { enabled: true }  | ref(:current_user) | true
+      ref(:current_user) | {}                 | ref(:current_user) | false
+      ref(:current_user) | { enabled: false } | ref(:current_user) | false
+    end
+
+    subject(:enabled) do
+      described_class.feature_enabled_from_application_settings?(user: user_arg)
+    end
 
-  describe '#help_url' do
-    it { expect(help_url).to match('/help/user/project/web_ide/_index.md#extension-marketplace') }
+    with_them do
+      before do
+        stub_feature_flags(vscode_extension_marketplace_settings: vscode_extension_marketplace_settings)
+        stub_application_setting(vscode_extension_marketplace: app_setting)
+      end
+
+      it { is_expected.to be(expectation) }
+    end
   end
 
-  describe '#help_preferences_url' do
-    it 'returns expected url' do
-      expect(described_class.help_preferences_url).to match(
-        '/help/user/profile/preferences.md#integrate-with-the-extension-marketplace'
-      )
+  describe '#marketplace_home_url' do
+    where(:vscode_extension_marketplace_settings, :app_setting, :expectation) do
+      false | {}                         | "https://open-vsx.org"
+      true  | {}                         | "https://open-vsx.org"
+      true  | ref(:custom_app_setting)   | "https://example.com:8444"
+      true  | ref(:open_vsx_app_setting) | "https://open-vsx.org"
+    end
+
+    subject(:marketplace_home_url) do
+      described_class.marketplace_home_url(user: current_user)
+    end
+
+    with_them do
+      before do
+        stub_feature_flags(vscode_extension_marketplace_settings: vscode_extension_marketplace_settings)
+        stub_application_setting(vscode_extension_marketplace: app_setting)
+      end
+
+      it { is_expected.to eq(expectation) }
     end
   end
 
-  describe '#user_preferences_url' do
-    it { expect(user_preferences_url).to match('/-/profile/preferences#integrations') }
+  describe '#help_preferences_url' do
+    subject(:url) { described_class.help_preferences_url }
+
+    it { is_expected.to match('/help/user/profile/preferences.md#integrate-with-the-extension-marketplace') }
   end
 
   describe '#webide_extension_marketplace_settings' do
-    subject(:webide_settings) { described_class.webide_extension_marketplace_settings(user: current_user) }
+    # rubocop:disable Layout/LineLength -- last parameter extens past line but is preferable to rubocop's suggestion
+    where(:web_ide_extensions_marketplace, :vscode_extension_marketplace_settings, :app_setting, :opt_in_status, :expectation) do
+      true  | false | {}                         | :enabled  | lazy { { enabled: true, vscode_settings: ::WebIde::ExtensionMarketplacePreset.open_vsx.values } }
+      true  | false | {}                         | :unset    | lazy { { enabled: false, reason: :opt_in_unset, help_url: /#{help_url}/, user_preferences_url: /#{user_preferences_url}/ } }
+      true  | false | {}                         | :disabled | lazy { { enabled: false, reason: :opt_in_disabled, help_url: /#{help_url}/, user_preferences_url: /#{user_preferences_url}/ } }
+      false | false | {}                         | :enabled  | lazy { { enabled: false, reason: :instance_disabled, help_url: /#{help_url}/ } }
+      true  | true  | {}                         | :enabled  | lazy { { enabled: false, reason: :instance_disabled, help_url: /#{help_url}/ } }
+      true  | true  | { enabled: false }         | :enabled  | lazy { { enabled: false, reason: :instance_disabled, help_url: /#{help_url}/ } }
+      true  | true  | ref(:custom_app_setting)   | :enabled  | lazy { { enabled: true, vscode_settings: custom_app_setting[:custom_values] } }
+      true  | true  | ref(:open_vsx_app_setting) | :enabled  | lazy { { enabled: true, vscode_settings: ::WebIde::ExtensionMarketplacePreset.open_vsx.values } }
+    end
+    # rubocop:enable Layout/LineLength
 
-    context 'when instance enabled' do
-      before do
-        stub_feature_flags(
-          web_ide_extensions_marketplace: current_user,
-          vscode_web_ide: current_user
-        )
-      end
+    subject(:webide_settings) { described_class.webide_extension_marketplace_settings(user: current_user) }
 
-      it 'when user opt in enabled, returns enabled settings' do
-        current_user.update!(extensions_marketplace_opt_in_status: :enabled)
+    before do
+      stub_feature_flags(
+        vscode_extension_marketplace_settings: vscode_extension_marketplace_settings,
+        web_ide_extensions_marketplace: web_ide_extensions_marketplace,
+        vscode_web_ide: true
+      )
 
-        expect(webide_settings).to match({
-          enabled: true,
-          vscode_settings: hash_including(default_vscode_settings)
-        })
-      end
+      stub_application_setting(vscode_extension_marketplace: app_setting)
 
-      context 'when user opt in disabled' do
-        where(:opt_in_status, :reason) do
-          :unset | :opt_in_unset
-          :disabled | :opt_in_disabled
-        end
-
-        with_them do
-          it 'returns disabled settings' do
-            current_user.update!(extensions_marketplace_opt_in_status: opt_in_status)
-
-            expect(webide_settings).to match({
-              enabled: false,
-              reason: reason,
-              help_url: /#{help_url}/,
-              user_preferences_url: /#{user_preferences_url}/
-            })
-          end
-        end
-      end
+      current_user.update!(extensions_marketplace_opt_in_status: opt_in_status)
     end
 
-    context 'when instance disabled' do
-      it 'returns disabled settings and help url' do
-        expect(webide_settings).to match({
-          enabled: false,
-          reason: :instance_disabled,
-          help_url: /#{help_url}/
-        })
-      end
+    with_them do
+      it { is_expected.to match(expectation) }
     end
   end
 end
diff --git a/spec/lib/web_ide/settings/extension_marketplace_generator_spec.rb b/spec/lib/web_ide/settings/extension_marketplace_generator_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..891400204966fe71a0a08a39292ef4bf8a8ec870
--- /dev/null
+++ b/spec/lib/web_ide/settings/extension_marketplace_generator_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require "fast_spec_helper"
+
+RSpec.describe WebIde::Settings::ExtensionMarketplaceGenerator, feature_category: :web_ide do
+  using RSpec::Parameterized::TableSyntax
+
+  let(:user_class) do
+    stub_const(
+      "User",
+      Class.new do
+        def flipper_id
+          "UserStub"
+        end
+      end
+    )
+  end
+
+  let(:user) { user_class.new }
+  let(:requested_setting_names) { [:vscode_extension_marketplace] }
+  let(:custom_app_settings) do
+    {
+      "enabled" => false,
+      "preset" => "custom",
+      "custom_values" => {
+        "item_url" => "abc",
+        "service_url" => "def",
+        "resource_url_template" => "ghi"
+      }
+    }
+  end
+
+  let(:context) do
+    {
+      requested_setting_names: requested_setting_names,
+      options: { user: user },
+      settings: {}
+    }
+  end
+
+  subject(:result) { described_class.generate(context)[:settings][:vscode_extension_marketplace] }
+
+  before do
+    allow(Feature).to receive(:enabled?).with(:vscode_extension_marketplace_settings, user).and_return(settings_flag)
+    allow(Gitlab::CurrentSettings).to receive(:vscode_extension_marketplace).and_return(app_setting)
+  end
+
+  describe 'default (with setting requested)' do
+    where(:settings_flag, :app_setting, :expectation) do
+      false | {}                         | ::WebIde::ExtensionMarketplacePreset.open_vsx.values
+      true  | {}                         | ::WebIde::ExtensionMarketplacePreset.open_vsx.values
+      true  | { "preset" => 'open_vsx' } | ::WebIde::ExtensionMarketplacePreset.open_vsx.values
+      true  | ref(:custom_app_settings)  | { item_url: "abc", service_url: "def", resource_url_template: "ghi" }
+      # This should never happen, but lets test it anyways
+      true  | { "preset" => 'DNE' }      | nil
+    end
+
+    with_them do
+      it { is_expected.to eq(expectation) }
+    end
+  end
+
+  describe 'without setting requested' do
+    let(:requested_setting_names) { [] }
+    let(:settings_flag) { true }
+    let(:app_setting) { custom_app_settings }
+
+    it { is_expected.to be_nil }
+  end
+end
diff --git a/spec/lib/web_ide/settings/extension_marketplace_home_url_generator_spec.rb b/spec/lib/web_ide/settings/extension_marketplace_home_url_generator_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..357515e289f61161559a7c09529d74f17a2a780b
--- /dev/null
+++ b/spec/lib/web_ide/settings/extension_marketplace_home_url_generator_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "fast_spec_helper"
+
+RSpec.describe WebIde::Settings::ExtensionMarketplaceHomeUrlGenerator, feature_category: :web_ide do
+  using RSpec::Parameterized::TableSyntax
+
+  let(:vscode_extension_marketplace) { {} }
+  let(:requested_setting_names) { [:vscode_extension_marketplace_home_url] }
+  let(:context) do
+    {
+      requested_setting_names: requested_setting_names,
+      settings: {
+        vscode_extension_marketplace: vscode_extension_marketplace
+      }
+    }
+  end
+
+  subject(:result) { described_class.generate(context)[:settings][:vscode_extension_marketplace_home_url] }
+
+  where(:requested_setting_names, :vscode_extension_marketplace, :expectation) do
+    [:vscode_extension_marketplace_home_url] | {}                                          | ''
+    [:vscode_extension_marketplace_home_url] | { item_url: 'https://example.com/foo/bar' } | 'https://example.com'
+    [:vscode_extension_marketplace_home_url] | { item_url: 'https://example.com:123/foo' } | 'https://example.com:123'
+    [:vscode_extension_marketplace_home_url] | { item_url: 'not really a thing...' }       | ''
+    []                                       | { item_url: 'https://example.com:123/foo' } | nil
+  end
+
+  with_them do
+    it { is_expected.to eq(expectation) }
+  end
+end
diff --git a/spec/lib/web_ide/settings/extension_marketplace_metadata_generator_spec.rb b/spec/lib/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
index 1e3f42e267e42dd7eef855b2e94b51156b3b9736..6f768dce9f5b221e2e3e423831491452be9036d3 100644
--- a/spec/lib/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
+++ b/spec/lib/web_ide/settings/extension_marketplace_metadata_generator_spec.rb
@@ -46,25 +46,39 @@
     :opt_in_status,
     :flag_exists,
     :flag_enabled,
+    :app_settings_enabled,
     :expected_vscode_extension_marketplace_metadata
   ) do
     # @formatter:off - Turn off RubyMine autoformatting
 
-    # user exists | opt_in_status | flag exists | flag_enabled | expected_settings
-    false         | :undefined    | false       | :undefined   | { enabled: false, disabled_reason: :no_user }
-    false         | :undefined    | true        | true         | { enabled: false, disabled_reason: :no_user }
-    true          | :unset        | false       | :undefined   | { enabled: false, disabled_reason: :no_flag }
-    true          | :unset        | true        | false        | { enabled: false, disabled_reason: :instance_disabled }
-    true          | :unset        | true        | true         | { enabled: false, disabled_reason: :opt_in_unset }
-    true          | :disabled     | true        | true         | { enabled: false, disabled_reason: :opt_in_disabled }
-    true          | :enabled      | true        | true         | { enabled: true }
-    true          | :invalid      | true        | true         | RuntimeError
+    # rubocop:disable Layout/LineLength -- Parameterized rows overflow and its better than the alternative
+    # user exists | opt_in_status | flag exists | flag_enabled | app_settings_enabled | expected_settings
+    false         | :undefined    | false       | :undefined   | true                 | { enabled: false, disabled_reason: :no_user }
+    false         | :undefined    | true        | true         | true                 | { enabled: false, disabled_reason: :no_user }
+    true          | :unset        | false       | :undefined   | true                 | { enabled: false, disabled_reason: :no_flag }
+    true          | :unset        | true        | false        | true                 | { enabled: false, disabled_reason: :instance_disabled }
+    true          | :unset        | true        | true         | true                 | { enabled: false, disabled_reason: :opt_in_unset }
+    true          | :disabled     | true        | true         | true                 | { enabled: false, disabled_reason: :opt_in_disabled }
+    true          | :enabled      | true        | true         | false                | { enabled: false, disabled_reason: :instance_disabled }
+    true          | :enabled      | true        | true         | true                 | { enabled: true }
+    true          | :invalid      | true        | true         | true                 | RuntimeError
+    # rubocop:enable Layout/LineLength
 
     # @formatter:on
   end
 
   with_them do
-    let(:user_class) { stub_const('User', Class.new) }
+    let(:user_class) do
+      stub_const(
+        "User",
+        Class.new do
+          def flipper_id
+            "UserStub"
+          end
+        end
+      )
+    end
+
     let(:user) { user_class.new }
     let(:enums) { stub_const('Enums::WebIde::ExtensionsMarketplaceOptInStatus', Class.new) }
 
@@ -80,6 +94,9 @@
       # EE feature has to be stubbed since we run EE code through CE tests
       allow(user).to receive(:enterprise_user?).and_return(false)
       allow(enums).to receive(:statuses).and_return({ unset: :unset, enabled: :enabled, disabled: :disabled })
+      allow(::WebIde::ExtensionMarketplace).to receive(:feature_enabled_from_application_settings?)
+        .with(user: user)
+        .and_return(app_settings_enabled)
     end
 
     it_behaves_like "extension marketplace settings"
diff --git a/spec/lib/web_ide/settings/extension_marketplace_validator_spec.rb b/spec/lib/web_ide/settings/extension_marketplace_validator_spec.rb
index 38c2dfc051db5cc9ff715ee16154841b85aa5f2d..8fa1e69b0011fd42f9feb0a648cf8d9b4ce6ca6a 100644
--- a/spec/lib/web_ide/settings/extension_marketplace_validator_spec.rb
+++ b/spec/lib/web_ide/settings/extension_marketplace_validator_spec.rb
@@ -12,10 +12,7 @@
     {
       service_url: service_url,
       item_url: item_url,
-      resource_url_template: resource_url_template,
-      control_url: "",
-      nls_base_url: "",
-      publisher_url: ""
+      resource_url_template: resource_url_template
     }
   end
 
diff --git a/spec/lib/web_ide/settings/main_spec.rb b/spec/lib/web_ide/settings/main_spec.rb
index 3629c4b71095235405ec4287d78708ec0e8821d3..613232c27340327461ba61b7be8b04d73e6a6177 100644
--- a/spec/lib/web_ide/settings/main_spec.rb
+++ b/spec/lib/web_ide/settings/main_spec.rb
@@ -9,6 +9,8 @@
   let(:rop_steps) do
     [
       [WebIde::Settings::SettingsInitializer, :map],
+      [WebIde::Settings::ExtensionMarketplaceGenerator, :map],
+      [WebIde::Settings::ExtensionMarketplaceHomeUrlGenerator, :map],
       [WebIde::Settings::ExtensionMarketplaceMetadataGenerator, :map],
       [Gitlab::Fp::Settings::EnvVarOverrideProcessor, :and_then],
       [WebIde::Settings::ExtensionMarketplaceValidator, :and_then],
diff --git a/spec/lib/web_ide/settings/settings_initializer_spec.rb b/spec/lib/web_ide/settings/settings_initializer_spec.rb
index e22a16f55a4abeec023c3d9a0a72b13370832a65..468b1138bbf48523ec3228b594434f06684f7752 100644
--- a/spec/lib/web_ide/settings/settings_initializer_spec.rb
+++ b/spec/lib/web_ide/settings/settings_initializer_spec.rb
@@ -18,17 +18,12 @@
         requested_setting_names: [
           :vscode_extension_marketplace,
           :vscode_extension_marketplace_metadata,
+          :vscode_extension_marketplace_home_url,
           :vscode_extension_marketplace_view_model
         ],
         settings: {
-          vscode_extension_marketplace: {
-            control_url: "",
-            item_url: "https://open-vsx.org/vscode/item",
-            nls_base_url: "",
-            publisher_url: "",
-            resource_url_template: 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
-            service_url: "https://open-vsx.org/vscode/gallery"
-          },
+          vscode_extension_marketplace: {},
+          vscode_extension_marketplace_home_url: "",
           vscode_extension_marketplace_metadata: {
             enabled: false,
             disabled_reason: :instance_disabled
@@ -41,6 +36,7 @@
         },
         setting_types: {
           vscode_extension_marketplace: Hash,
+          vscode_extension_marketplace_home_url: String,
           vscode_extension_marketplace_metadata: Hash,
           vscode_extension_marketplace_view_model: Hash
         },
diff --git a/spec/lib/web_ide/settings/settings_integration_spec.rb b/spec/lib/web_ide/settings/settings_integration_spec.rb
index 532d9ceb0b4420fe7c5703e0b6ba9a6a24319478..ddf8c8f526f427d5f7f097c0c44759ccb759c184 100644
--- a/spec/lib/web_ide/settings/settings_integration_spec.rb
+++ b/spec/lib/web_ide/settings/settings_integration_spec.rb
@@ -9,10 +9,7 @@
     {
       service_url: "https://open-vsx.org/vscode/gallery",
       item_url: "https://open-vsx.org/vscode/item",
-      resource_url_template: 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}',
-      control_url: "",
-      nls_base_url: "",
-      publisher_url: ""
+      resource_url_template: 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}'
     }
   end
 
@@ -103,10 +100,7 @@
         expected_value = {
           item_url: "https://open-vsx.org/vscode/item",
           resource_url_template: "https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}",
-          service_url: "https://open-vsx.org/vscode/gallery",
-          control_url: "",
-          nls_base_url: "",
-          publisher_url: ""
+          service_url: "https://open-vsx.org/vscode/gallery"
         }
 
         expect(vscode_extension_marketplace_setting).to eq(expected_value)
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 9b1fde71a6aa687c41930e072e2506fef365de5a..4c928cb0e3f1408cbb5ab28f6d963b6892b3fe8a 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -292,10 +292,37 @@
   end
 
   describe 'routing table switch' do
+    shared_examples 'both tables are set up for partitioning' do
+      let(:tables_registered_for_sync) { Gitlab::Database::Partitioning.send(:registered_for_sync) }
+      it 'registers both web_hook_logs and web_hook_logs_daily for partitioning' do
+        expect(tables_registered_for_sync.map(&:table_name)).to include('web_hook_logs_daily', 'web_hook_logs')
+      end
+
+      it 'registers web_hook_logs for monthly partitioning' do
+        web_hook_logs_entries = tables_registered_for_sync.select { |model| model.table_name == 'web_hook_logs' }
+        expect(web_hook_logs_entries.count).to eq(1)
+        web_hook_logs_entry = web_hook_logs_entries.first
+        expect(web_hook_logs_entry.partitioning_strategy).to be_a(Gitlab::Database::Partitioning::Time::MonthlyStrategy)
+        expect(web_hook_logs_entry.partitioning_strategy.retain_for).to eq(1.month)
+      end
+
+      it 'registers web_hook_logs_daily for daily partitioning' do
+        web_hook_logs_daily_entries = tables_registered_for_sync
+                                        .select { |model| model.table_name == 'web_hook_logs_daily' }
+        expect(web_hook_logs_daily_entries.count).to eq(1)
+        web_hook_logs_daily_entry = web_hook_logs_daily_entries.first
+        expect(web_hook_logs_daily_entry.partitioning_strategy)
+          .to be_a(Gitlab::Database::Partitioning::Time::DailyStrategy)
+        expect(web_hook_logs_daily_entry.partitioning_strategy.retain_for).to eq(14.days)
+      end
+    end
+
     context 'with ff enabled' do
       it 'returns daily partitioned table' do
         expect(described_class.table_name).to eq('web_hook_logs_daily')
       end
+
+      it_behaves_like 'both tables are set up for partitioning'
     end
 
     context 'with ff disabled' do
@@ -306,6 +333,8 @@
       it 'returns monthly partitioned table' do
         expect(described_class.table_name).to eq('web_hook_logs')
       end
+
+      it_behaves_like 'both tables are set up for partitioning'
     end
   end
 end
diff --git a/yarn.lock b/yarn.lock
index 0da735c6ffbcc93ec03039ba237dc5af2e0e1dd7..55e64491ca090841491c1d4f27151d3cb9015057 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1422,10 +1422,10 @@
   resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
   integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
 
-"@gitlab/query-language-rust@0.4.0":
-  version "0.4.0"
-  resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.4.0.tgz#401be9312b3a18b026cd5e305dac579db8cc2e28"
-  integrity sha512-3tX5cFNxPakPqUzIXo2oD+yaI5x39fD4tA45SBDksHZYvojNIrohj1r2wH40w57OQnLyM1zMIocuZ95Sby40dQ==
+"@gitlab/query-language-rust@0.4.2":
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.4.2.tgz#82fa765fa75bd28eb2af08575203eeab95e214e4"
+  integrity sha512-ZPujJzNAJlIYPU57C+yUrzpIKL17yodVU0zBakqcCl/rtTpMhBEaLwY3Nv0V+SVPCG5h/e/up3TALqJyZAC4zg==
 
 "@gitlab/stylelint-config@6.2.2":
   version "6.2.2"