Repository: getsentry/sentry
Pull request: feat(metrics) Handle Investigation rules limit reached in UI 
This PR handles investigation rules limit reached errors by displaying a 
specific message to the user indicating that the maximum number of allowed
rules has been reached.

resolves https://github.com/getsentry/sentry/issues/59681

Problem statement:
Stop users from creating more than 50 investigation rules per project
Users should not be able to create more than 50 investigation rules that apply to a project.

Need to figure out the user experience for limiting this.

We could simply disable the rule creation if adding another rule would cause any project to go over the limit.
This would be quite confusing for users, they would see that there are no samples but at the same time they will not get the UI that offers them to create a rule.

We could also keep the button and generate an error when the user tries to create a rule ( with some feedback... projects X,Y,Z would get more that 50 active investigation rules and that's not allowed).
Second option would make it clearer for the user what's going on but would frustrate them by first inviting them to create a rule only to be told that they can't do it when they press the button.

Finally we could just check and if we don't have enough samples and we also have too many rules we could place a message (similar to the one that says "Collecting samples since ... ") saying something about not being able create investigation rules because there are already too many in progress.

A complete solution would also include a UI for managing existing investigation rules, currently they will persist as active for 2 days regardless if they have collected enough data from the user's point of view or not.

Hints from issue discussion:
Let's display the Get Samples button, and if they press it and would go over 50 rules per project, the frontend will display the red toast notification (bottom right) saying "We are already getting samples for maximum amount of concurrent queries". - wording in progress

Commit message: Display different message for investigation limit reached error

Patch to review:
diff --git a/static/app/components/dynamicSampling/investigationRule.spec.tsx b/static/app/components/dynamicSampling/investigationRule.spec.tsx
index 0892eef3997424..317d99cc49a277 100644
--- a/static/app/components/dynamicSampling/investigationRule.spec.tsx
+++ b/static/app/components/dynamicSampling/investigationRule.spec.tsx
@@ -256,6 +256,37 @@ describe('InvestigationRule', function () {
     await tick();
     expect(createRule).toHaveBeenCalledTimes(1);
     // we should show some error that the rule could not be created
-    expect(addErrorMessage).toHaveBeenCalledTimes(1);
+    expect(addErrorMessage).toHaveBeenCalledWith('Unable to create investigation rule');
+  });
+
+  it('should show notify the user when too many rules have been created', async function () {
+    initComponentEnvironment({hasFeature: true, hasRule: false});
+    const createRule = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/dynamic-sampling/custom-rules/',
+      method: 'POST',
+      statusCode: 429,
+      body: {query: ['some-error']},
+    });
+
+    render(
+      <InvestigationRuleCreation buttonProps={{}} eventView={eventView} numSamples={1} />,
+      {organization}
+    );
+
+    // wait for the button to appear
+    const button = await screen.findByText(buttonText);
+    expect(button).toBeInTheDocument();
+    // we should  not be showing the label
+    const labels = screen.queryAllByText(labelText);
+    expect(labels).toHaveLength(0);
+    // now the user creates a rule
+    fireEvent.click(button);
+
+    await tick();
+    expect(createRule).toHaveBeenCalledTimes(1);
+    // we should show some error that the rule could not be created
+    expect(addErrorMessage).toHaveBeenCalledWith(
+      'You have reached the maximum number of concurrent investigation rules allowed'
+    );
   });
 });
diff --git a/static/app/components/dynamicSampling/investigationRule.tsx b/static/app/components/dynamicSampling/investigationRule.tsx
index 676e9ffc14fb87..733ae7f3f18315 100644
--- a/static/app/components/dynamicSampling/investigationRule.tsx
+++ b/static/app/components/dynamicSampling/investigationRule.tsx
@@ -121,7 +121,7 @@ function useCreateInvestigationRuleMutation(vars: CreateCustomRuleVariables) {
   const queryClient = useQueryClient();
   const {mutate} = useMutation<
     CustomDynamicSamplingRule,
-    Error,
+    RequestError,
     CreateCustomRuleVariables
   >({
     mutationFn: (variables: CreateCustomRuleVariables) => {
@@ -145,8 +145,17 @@ function useCreateInvestigationRuleMutation(vars: CreateCustomRuleVariables) {
         success: true,
       });
     },
-    onError: (_error: Error) => {
-      addErrorMessage(t('Unable to create investigation rule'));
+    onError: (_error: RequestError) => {
+      if (_error.status === 429) {
+        addErrorMessage(
+          t(
+            'You have reached the maximum number of concurrent investigation rules allowed'
+          )
+        );
+      } else {
+        addErrorMessage(t('Unable to create investigation rule'));
+      }
+
       trackAnalytics('dynamic_sampling.custom_rule_add', {
         organization: vars.organization,
         projects: vars.projects,


Code review comments:
diff_hunk: @@ -256,6 +256,37 @@ describe('InvestigationRule', function () {
     await tick();
     expect(createRule).toHaveBeenCalledTimes(1);
     // we should show some error that the rule could not be created
-    expect(addErrorMessage).toHaveBeenCalledTimes(1);
+    expect(addErrorMessage).toHaveBeenCalledWith('Unable to create investigation rule');
+  });
+
+  it('should show notify the user when too many rules have been created', async function () {
+    initComponentEnvironment({hasFeature: true, hasRule: false});
+    const createRule = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/dynamic-sampling/custom-rules/',
+      method: 'POST',
+      statusCode: 429,
+      body: {query: ['some-error']},
+    });
+
+    render(
+      <InvestigationRuleCreation buttonProps={{}} eventView={eventView} numSamples={1} />,
+      {organization}
+    );
+
+    // wait for the button to appear
+    const button = await screen.findByText(buttonText);
+    expect(button).toBeInTheDocument();
+    // we should  not be showing the label
+    const labels = screen.queryAllByText(labelText);
+    expect(labels).toHaveLength(0);
+    // now the user creates a rule
+    fireEvent.click(button);
+
+    await tick();
path: static/app/components/dynamicSampling/investigationRule.spec.tsx
text: @user1:
You can use userEvent, then the `await tick()` should not be necessary.
```suggestion
    await userEvent.click((button);

```