Repository: getsentry/sentry
Pull request #91550: chore(plugins): No new plugin installation - FE
Author: login: sentaur-athena
Labels: name: Scope: Frontend
description: Automatically applied to PRs that change frontend components
name: Scope: Backend
description: Automatically applied to PRs that change backend components
Created: 2025-05-13T17:24:51Z | Merged: 2025-05-15T18:14:47Z

Note: backend piece will be removed after merging https://github.com/getsentry/sentry/pull/91142/

Frontend piece no not allow installation of plugins on new projects.
1. New plugin users cannot see them in search as result of previous PR
2. New plugin users can see installation page by going there directly with URL but installation button is disabled
3. Existing plugin installations work as expected
4. Existing plugin customers cannot install them on new projects.

Next is updating the docs.
Project release settings -  before
![Screenshot 2025-05-08 at 11 30 55 AM](https://github.com/user-attachments/assets/48cf1908-8600-42ab-9e83-f96415fb649b)
Project release settings - after
![Screenshot 2025-05-13 at 10 19 05 AM](https://github.com/user-attachments/assets/74a88349-6751-4406-9f2f-66beb664e9d6)

Plugin installation page after
![Screenshot 2025-05-08 at 10 53 32 AM](https://github.com/user-attachments/assets/938ab2c2-22fe-49ad-8efd-59e8af7ac691)

Project - legacy integration lists: before it had all the plugins
![Screenshot 2025-05-13 at 10 18 00 AM](https://github.com/user-attachments/assets/0b8f44a7-48cb-4145-9aa5-da2a91ee770f)

After: It only has autotags. We don't deprecate them.
![Screenshot 2025-05-13 at 10 19 16 AM](https://github.com/user-attachments/assets/8f4c1cc8-f9b5-4be1-a936-654b74352d3c)







Diff:
diff --git a/static/app/components/inactivePlugins.spec.tsx b/static/app/components/inactivePlugins.spec.tsx
deleted file mode 100644
index 32f4b76699a8..000000000000
--- a/static/app/components/inactivePlugins.spec.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import {PluginsFixture} from 'sentry-fixture/plugins';
-
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
-
-import InactivePlugins from 'sentry/components/inactivePlugins';
-
-describe('InactivePlugins', function () {
-  it('renders null when no plugins', function () {
-    const {container} = render(
-      <InactivePlugins plugins={[]} onEnablePlugin={() => {}} />
-    );
-    expect(container).toBeEmptyDOMElement();
-  });
-
-  it('renders plugins list', function () {
-    render(<InactivePlugins onEnablePlugin={() => {}} plugins={PluginsFixture()} />);
-  });
-
-  it('enables a plugin', async function () {
-    const enableFn = jest.fn();
-    const plugins = PluginsFixture();
-    render(<InactivePlugins onEnablePlugin={enableFn} plugins={plugins} />);
-    await userEvent.click(screen.getByRole('button', {name: plugins[0]!.name}));
-    expect(enableFn).toHaveBeenCalledWith(expect.objectContaining(plugins[0]));
-  });
-});
diff --git a/static/app/components/inactivePlugins.tsx b/static/app/components/inactivePlugins.tsx
deleted file mode 100644
index 74124a62aa93..000000000000
--- a/static/app/components/inactivePlugins.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import styled from '@emotion/styled';
-
-import {Flex} from 'sentry/components/container/flex';
-import {Button} from 'sentry/components/core/button';
-import Panel from 'sentry/components/panels/panel';
-import PanelBody from 'sentry/components/panels/panelBody';
-import PanelHeader from 'sentry/components/panels/panelHeader';
-import TextOverflow from 'sentry/components/textOverflow';
-import {t} from 'sentry/locale';
-import {PluginIcon} from 'sentry/plugins/components/pluginIcon';
-import {space} from 'sentry/styles/space';
-import type {Plugin} from 'sentry/types/integrations';
-
-type Props = {
-  onEnablePlugin: (plugin: Plugin) => void;
-  plugins: Plugin[];
-  disabled?: boolean;
-};
-
-function InactivePlugins({disabled, plugins, onEnablePlugin}: Props) {
-  if (plugins.length === 0) {
-    return null;
-  }
-
-  return (
-    <Panel>
-      <PanelHeader>{t('Inactive Integrations')}</PanelHeader>
-
-      <PanelBody>
-        <Plugins>
-          {plugins.map(plugin => (
-            <IntegrationButton
-              disabled={disabled}
-              title={
-                disabled ? t('You do not have permission to set up an integration.') : ''
-              }
-              key={plugin.id}
-              onClick={() => onEnablePlugin(plugin)}
-              className={`ref-plugin-enable-${plugin.id}`}
-            >
-              <Flex align="center" justify="center" gap={space(1)}>
-                <PluginIcon pluginId={plugin.id} />
-                <TextOverflow>{plugin.shortName || plugin.name}</TextOverflow>
-              </Flex>
-            </IntegrationButton>
-          ))}
-        </Plugins>
-      </PanelBody>
-    </Panel>
-  );
-}
-
-const Plugins = styled('div')`
-  display: flex;
-  padding: ${space(1)};
-  flex: 1;
-  flex-wrap: wrap;
-`;
-
-const IntegrationButton = styled(Button)`
-  text-align: center;
-  text-transform: uppercase;
-  min-width: 175px;
-  margin: ${space(1)};
-`;
-
-export default InactivePlugins;
diff --git a/static/app/components/pluginList.tsx b/static/app/components/pluginList.tsx
index 85d29edfbb54..5bd4827b8ed0 100644
--- a/static/app/components/pluginList.tsx
+++ b/static/app/components/pluginList.tsx
@@ -1,6 +1,4 @@
-import {disablePlugin, enablePlugin} from 'sentry/actionCreators/plugins';
-import {hasEveryAccess} from 'sentry/components/acl/access';
-import InactivePlugins from 'sentry/components/inactivePlugins';
+import {disablePlugin} from 'sentry/actionCreators/plugins';
 import Panel from 'sentry/components/panels/panel';
 import PanelItem from 'sentry/components/panels/panelItem';
 import PluginConfig from 'sentry/components/pluginConfig';
@@ -22,20 +20,7 @@ function PluginList({
   project,
   pluginList,
   onDisablePlugin = () => {},
-  onEnablePlugin = () => {},
 }: Props) {
-  const hasWriteAccess = hasEveryAccess(['project:write'], {organization, project});
-
-  const handleEnablePlugin = (plugin: Plugin) => {
-    enablePlugin({
-      projectId: project.slug,
-      orgId: organization.slug,
-      pluginId: plugin.slug,
-    });
-
-    onEnablePlugin(plugin);
-  };
-
   const handleDisablePlugin = (plugin: Plugin) => {
     disablePlugin({
       projectId: project.slug,
@@ -68,12 +53,6 @@ function PluginList({
             onDisablePlugin={handleDisablePlugin}
           />
         ))}
-
-      <InactivePlugins
-        disabled={!hasWriteAccess}
-        plugins={pluginList.filter(p => !p.enabled && !p.isHidden)}
-        onEnablePlugin={handleEnablePlugin}
-      />
     </div>
   );
 }
diff --git a/static/app/views/settings/organizationIntegrations/pluginDetailedView.spec.tsx b/static/app/views/settings/organizationIntegrations/pluginDetailedView.spec.tsx
index 73f4ad322ea9..b98895b678e6 100644
--- a/static/app/views/settings/organizationIntegrations/pluginDetailedView.spec.tsx
+++ b/static/app/views/settings/organizationIntegrations/pluginDetailedView.spec.tsx
@@ -3,12 +3,7 @@ import {PluginFixture} from 'sentry-fixture/plugin';
 import {ProjectFixture} from 'sentry-fixture/project';
 import {RouterFixture} from 'sentry-fixture/routerFixture';
 
-import {
-  render,
-  renderGlobalModal,
-  screen,
-  userEvent,
-} from 'sentry-test/reactTestingLibrary';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import type {PluginWithProjectList} from 'sentry/types/integrations';
 import PluginDetailedView from 'sentry/views/settings/organizationIntegrations/pluginDetailedView';
@@ -53,10 +48,6 @@ describe('PluginDetailedView', function () {
 
     expect(await screen.findByText(plugin.name)).toBeInTheDocument();
     expect(screen.getByText('Installed')).toBeInTheDocument();
-    await userEvent.click(screen.getByRole('button', {name: 'Add to Project'}));
-
-    renderGlobalModal();
-    expect(await screen.findByRole('dialog')).toBeInTheDocument();
   });
 
   it('view configurations', async function () {
diff --git a/static/app/views/settings/organizationIntegrations/pluginDetailedView.tsx b/static/app/views/settings/organizationIntegrations/pluginDetailedView.tsx
index 0b3c51804726..d868d50aaec0 100644
--- a/static/app/views/settings/organizationIntegrations/pluginDetailedView.tsx
+++ b/static/app/views/settings/organizationIntegrations/pluginDetailedView.tsx
@@ -1,15 +1,12 @@
 import {Fragment, useCallback, useEffect, useMemo} from 'react';
-import styled from '@emotion/styled';
 
-import {openModal} from 'sentry/actionCreators/modal';
 import {hasEveryAccess} from 'sentry/components/acl/access';
-import ContextPickerModal from 'sentry/components/contextPickerModal';
 import {Button} from 'sentry/components/core/button';
+import {Tooltip} from 'sentry/components/core/tooltip';
 import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
 import {PluginIcon} from 'sentry/plugins/components/pluginIcon';
-import {space} from 'sentry/styles/space';
 import type {
   IntegrationInstallationStatus,
   PluginProjectItem,
@@ -22,8 +19,6 @@ import {
   useApiQuery,
   useQueryClient,
 } from 'sentry/utils/queryClient';
-import normalizeUrl from 'sentry/utils/url/normalizeUrl';
-import {useNavigate} from 'sentry/utils/useNavigate';
 import useOrganization from 'sentry/utils/useOrganization';
 import {useParams} from 'sentry/utils/useParams';
 import useProjects from 'sentry/utils/useProjects';
@@ -36,7 +31,6 @@ import type {IntegrationTab} from 'sentry/views/settings/organizationIntegration
 import IntegrationLayout from 'sentry/views/settings/organizationIntegrations/detailedView/integrationLayout';
 import {useIntegrationTabs} from 'sentry/views/settings/organizationIntegrations/detailedView/useIntegrationTabs';
 import InstalledPlugin from 'sentry/views/settings/organizationIntegrations/installedPlugin';
-import RequestIntegrationButton from 'sentry/views/settings/organizationIntegrations/integrationRequest/RequestIntegrationButton';
 import PluginDeprecationAlert from 'sentry/views/settings/organizationIntegrations/pluginDeprecationAlert';
 
 function makePluginQueryKey({
@@ -57,7 +51,6 @@ function PluginDetailedView() {
 
   const organization = useOrganization();
   const queryClient = useQueryClient();
-  const navigate = useNavigate();
   const {integrationSlug} = useParams<{integrationSlug: string}>();
 
   const {projects} = useProjects();
@@ -187,59 +180,18 @@ function PluginDetailedView() {
     [plugin, organization.slug, integrationSlug, queryClient]
   );
 
-  const handleAddToProject = useCallback(() => {
-    if (!plugin) {
-      return;
-    }
-    trackIntegrationAnalytics('integrations.plugin_add_to_project_clicked', {
-      view: 'integrations_directory_integration_detail',
-      integration: integrationSlug,
-      integration_type: integrationType,
-      already_installed: installationStatus !== 'Not Installed', // pending counts as installed here
-      organization,
-    });
-    openModal(
-      modalProps => (
-        <ContextPickerModal
-          {...modalProps}
-          nextPath={`/settings/${organization.slug}/projects/:projectId/plugins/${plugin.id}/`}
-          needProject
-          needOrg={false}
-          onFinish={to => {
-            modalProps.closeModal();
-            navigate(normalizeUrl(to));
-          }}
-        />
-      ),
-      {closeEvents: 'escape-key'}
+  const renderDeprecatedButton = useCallback(() => {
+    return (
+      <Tooltip
+        title={t(
+          'This Plugin is deprecated and not available to install on new projects.'
+        )}
+        isHoverable
+      >
+        <Button disabled>Add to Project</Button>
+      </Tooltip>
     );
-  }, [integrationSlug, installationStatus, navigate, organization, plugin]);
-
-  const renderTopButton = useCallback(
-    (disabledFromFeatures: boolean, userHasAccess: boolean) => {
-      if (userHasAccess) {
-        return (
-          <AddButton
-            data-test-id="install-button"
-            disabled={disabledFromFeatures}
-            onClick={handleAddToProject}
-            size="sm"
-            priority="primary"
-          >
-            {t('Add to Project')}
-          </AddButton>
-        );
-      }
-      return (
-        <RequestIntegrationButton
-          name={integrationName}
-          slug={integrationSlug}
-          type={integrationType}
-        />
-      );
-    },
-    [handleAddToProject, integrationName, integrationSlug, integrationType]
-  );
+  }, []);
 
   const renderConfigurations = useCallback(() => {
     if (plugin?.projectList.length) {
@@ -274,20 +226,8 @@ function PluginDetailedView() {
         </Fragment>
       );
     }
-    return (
-      <IntegrationLayout.EmptyConfigurations
-        action={
-          <IntegrationLayout.AddInstallButton
-            featureData={featureData}
-            hideButtonIfDisabled
-            renderTopButton={renderTopButton}
-            requiresAccess
-          />
-        }
-      />
-    );
+    return null;
   }, [
-    featureData,
     handlePluginEnableStatus,
     handleResetConfiguration,
     installationStatus,
@@ -295,7 +235,6 @@ function PluginDetailedView() {
     integrationType,
     organization,
     plugin,
-    renderTopButton,
     projects,
   ]);
 
@@ -322,7 +261,7 @@ function PluginDetailedView() {
               featureData={featureData}
               hideButtonIfDisabled={false}
               requiresAccess={false}
-              renderTopButton={renderTopButton}
+              renderTopButton={renderDeprecatedButton}
             />
           }
           additionalCTA={null}
@@ -354,8 +293,4 @@ function PluginDetailedView() {
   );
 }
 
-const AddButton = styled(Button)`
-  margin-bottom: ${space(1)};
-`;
-
 export default withOrganization(PluginDetailedView);
diff --git a/static/app/views/settings/projectAlerts/settings.spec.tsx b/static/app/views/settings/projectAlerts/settings.spec.tsx
index 66cc3186accd..0885d76db607 100644
--- a/static/app/views/settings/projectAlerts/settings.spec.tsx
+++ b/static/app/views/settings/projectAlerts/settings.spec.tsx
@@ -1,8 +1,7 @@
-import {WebhookPluginConfigFixture} from 'sentry-fixture/integrationListDirectory';
 import {ProjectFixture} from 'sentry-fixture/project';
 
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import ProjectAlertSettings from 'sentry/views/settings/projectAlerts/settings';
 
@@ -55,35 +54,4 @@ describe('ProjectAlertSettings', () => {
       )
     ).toBeInTheDocument();
   });
-
-  it('enables webhook integration', async () => {
-    const pluginConfig = WebhookPluginConfigFixture({enabled: false});
-
-    MockApiClient.addMockResponse({
-      url: `/projects/${organization.slug}/${project.slug}/plugins/`,
-      method: 'GET',
-      body: [pluginConfig],
-    });
-    const enabledPluginMock = MockApiClient.addMockResponse({
-      url: `/projects/${organization.slug}/${project.slug}/plugins/${pluginConfig.id}/`,
-      method: 'POST',
-      body: '',
-    });
-    const getWebhookMock = MockApiClient.addMockResponse({
-      url: `/projects/${organization.slug}/${project.slug}/plugins/${pluginConfig.id}/`,
-      method: 'GET',
-      body: [{...pluginConfig, enabled: true}],
-    });
-
-    render(<ProjectAlertSettings canEditRule {...routerProps} />);
-
-    expect(
-      await screen.findByPlaceholderText('e.g. $shortID - $title')
-    ).toBeInTheDocument();
-    await userEvent.click(screen.getByRole('button', {name: 'WebHooks'}));
-
-    expect(await screen.findByRole('button', {name: 'Test Plugin'})).toBeInTheDocument();
-    expect(enabledPluginMock).toHaveBeenCalled();
-    expect(getWebhookMock).toHaveBeenCalled();
-  });
 });
diff --git a/static/app/views/settings/projectAlerts/settings.tsx b/static/app/views/settings/projectAlerts/settings.tsx
index 44d12fc24f1a..b85baa0890f7 100644
--- a/static/app/views/settings/projectAlerts/settings.tsx
+++ b/static/app/views/settings/projectAlerts/settings.tsx
@@ -90,10 +90,6 @@ function ProjectAlertSettings({canEditRule, params}: ProjectAlertSettingsProps)
     );
   };
 
-  const handleEnablePlugin = (plugin: Plugin) => {
-    updatePlugin(plugin, true);
-  };
-
   const handleDisablePlugin = (plugin: Plugin) => {
     updatePlugin(plugin, false);
   };
@@ -175,7 +171,6 @@ function ProjectAlertSettings({canEditRule, params}: ProjectAlertSettingsProps)
               pluginList={(pluginList ?? []).filter(
                 p => p.type === 'notification' && p.hasConfiguration
               )}
-              onEnablePlugin={handleEnablePlugin}
               onDisablePlugin={handleDisablePlugin}
             />
           )}
diff --git a/static/app/views/settings/projectDataForwarding/index.tsx b/static/app/views/settings/projectDataForwarding/index.tsx
index 8b101c1e31d8..05bfb5088cd1 100644
--- a/static/app/views/settings/projectDataForwarding/index.tsx
+++ b/static/app/views/settings/projectDataForwarding/index.tsx
@@ -129,7 +129,6 @@ function ProjectDataForwarding({project}: Props) {
     setPluginState(newPlugins);
   };
 
-  const onEnablePlugin = (plugin: Plugin) => updatePlugin(plugin, true);
   const onDisablePlugin = (plugin: Plugin) => updatePlugin(plugin, false);
 
   const hasAccess = hasEveryAccess(['project:write'], {organization, project});
@@ -140,7 +139,6 @@ function ProjectDataForwarding({project}: Props) {
         organization={organization}
         project={project}
         pluginList={forwardingPlugins()}
-        onEnablePlugin={onEnablePlugin}
         onDisablePlugin={onDisablePlugin}
       />
     ) : (
diff --git a/tests/acceptance/test_organization_plugin_detail_view.py b/tests/acceptance/test_organization_plugin_detail_view.py
index 83ee5fa3ac13..472be1b59941 100644
--- a/tests/acceptance/test_organization_plugin_detail_view.py
+++ b/tests/acceptance/test_organization_plugin_detail_view.py
@@ -28,17 +28,6 @@ def load_page(self, slug, configuration_tab=False):
         self.browser.get(url)
         self.browser.wait_until_not('[data-test-id="loading-indicator"]')
 
-    def test_opsgenie_add_to_project(self):
-        self.load_page("opsgenie")
-
-        detail_view_page = OrganizationAbstractDetailViewPage(browser=self.browser)
-        detail_view_page.click_install_button()
-
-        self.browser.click('[role="dialog"] [id$="option-0-0"]')
-        # check if we got to the configuration page with the form
-        self.browser.wait_until_not('[data-test-id="loading-indicator"]')
-        self.browser.wait_until_test_id("plugin-config")
-
     def test_uninstallation(self):
         self.plugin.set_option("api_key", "7c8951d1", self.project)
         self.plugin.set_option("alert_url", "https://api.opsgenie.com/v2/alerts", self.project)


Reviews:
user: login: sentaur-athena
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:27:58Z
user: login: sentaur-athena
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T17:29:38Z
user: login: sentaur-athena
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T22:08:37Z
user: login: cathteng
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T18:13:41Z

Review comments:
diff_hunk: @@ -175,7 +171,6 @@ function ProjectAlertSettings({canEditRule, params}: ProjectAlertSettingsProps)
               pluginList={(pluginList ?? []).filter(
                 p => p.type === 'notification' && p.hasConfiguration
               )}
-              onEnablePlugin={handleEnablePlugin}
               onDisablePlugin={handleDisablePlugin}
path: static/app/views/settings/projectAlerts/settings.tsx
user: login: sentaur-athena
body: I'm keeping disable plugin on this component so if existing users want to uninstall from this view they can. Otherwise this component can be deleted completely.
created_at: 2025-05-13T17:27:58Z
author_association: MEMBER
diff_hunk: @@ -175,7 +171,6 @@ function ProjectAlertSettings({canEditRule, params}: ProjectAlertSettingsProps)
               pluginList={(pluginList ?? []).filter(
                 p => p.type === 'notification' && p.hasConfiguration
               )}
-              onEnablePlugin={handleEnablePlugin}
               onDisablePlugin={handleDisablePlugin}
path: static/app/views/settings/projectAlerts/settings.tsx
user: login: sentaur-athena
body: Now that I'm looking I think I can delete more within <PluginList> 👀 
created_at: 2025-05-13T17:29:38Z
author_association: MEMBER
diff_hunk: @@ -175,7 +171,6 @@ function ProjectAlertSettings({canEditRule, params}: ProjectAlertSettingsProps)
               pluginList={(pluginList ?? []).filter(
                 p => p.type === 'notification' && p.hasConfiguration
               )}
-              onEnablePlugin={handleEnablePlugin}
               onDisablePlugin={handleDisablePlugin}
path: static/app/views/settings/projectAlerts/settings.tsx
user: login: sentaur-athena
body: I deleted more. Second comment is resolved. Keeping it open for visibility.
created_at: 2025-05-13T22:08:37Z
author_association: MEMBER
diff_hunk: @@ -53,10 +48,6 @@ describe('PluginDetailedView', function () {
 
     expect(await screen.findByText(plugin.name)).toBeInTheDocument();
     expect(screen.getByText('Installed')).toBeInTheDocument();
-    await userEvent.click(screen.getByRole('button', {name: 'Add to Project'}));
path: static/app/views/settings/organizationIntegrations/pluginDetailedView.spec.tsx
user: login: cathteng
body: nit: test that it's grayed out?
created_at: 2025-05-15T18:13:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T17:25:13Z
body: <!-- FRONTEND_BACKEND_WARNING -->
🚨 **Warning:** This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are **not** atomically deployed. If the changes are interdependent of each other, they **must** be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the [`#discuss-dev-infra` channel](https://app.slack.com/client/T024ZCV9U/CTJL7358X).
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-13T17:30:34Z
body: ## [Codecov](https://app.codecov.io/gh/getsentry/sentry/pull/91550?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getsentry) Report
All modified and coverable lines are covered by tests :white_check_mark:

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master   #91550   +/-   ##
=======================================
  Coverage   87.62%   87.62%           
=======================================
  Files       10330    10330           
  Lines      585770   585736   -34     
  Branches    22471    22468    -3     
=======================================
- Hits       513272   513248   -24     
+ Misses      72079    72069   -10     
  Partials      419      419           
```

</details>
author_association: NONE
user: login: sentaur-athena
created_at: 2025-05-13T20:15:21Z
body: Just had a meeting with CSM team and decided to pause this PR until we have data confirmed that users are not installing on new projects all the time.
author_association: MEMBER
user: login: sentaur-athena
created_at: 2025-05-14T23:49:10Z
body: [Queried](https://redash.getsentry.net/queries/8879/source) recent plugin installs. They're not many so moving forward with the PR.
author_association: MEMBER