6 Commits bb8a74e922 ... d68c2ebf4f

Author SHA1 Message Date
  Vjeran Grozdanić d68c2ebf4f ref(metrics) fix incorrect exception being raised (#73367) 3 days ago
  Vjeran Grozdanić b2cd1452ec fix(metrics) Prevent negative crash_free_rate value to be returned to the client (#73353) 3 days ago
  Priscila Oliveira 03fcab02fa feat(settings-metrics): Add extrapolate metrics field (#73287) 3 days ago
  Ogi b750c60cfb fix(metrics): skip dot escape for tags (#73371) 3 days ago
  Riccardo Busetti b7abc00cd0 chore(dynamic-sampling): Remove logging used for debugging (#73368) 3 days ago
  Priscila Oliveira c4b844e52a ref(settings-debug-files): Convert to functional component (#73269) 3 days ago

+ 4 - 0
src/sentry/api/endpoints/organization_release_health_data.py

@@ -11,6 +11,7 @@ from sentry.exceptions import InvalidParams
 from sentry.sentry_metrics.use_case_utils import get_use_case_id
 from sentry.snuba.metrics import DerivedMetricException, QueryDefinition, get_series
 from sentry.types.ratelimit import RateLimit, RateLimitCategory
+from sentry.utils import metrics
 from sentry.utils.cursors import Cursor, CursorResult
 
 
@@ -59,6 +60,9 @@ class OrganizationReleaseHealthDataEndpoint(OrganizationEndpoint):
                     use_case_id=get_use_case_id(request),
                     tenant_ids={"organization_id": organization.id},
                 )
+                # due to possible data corruption crash_free_rate value can be less than 0,
+                # which is not valid behavior, so those values have to be bottom capped at 0
+                metrics.ensure_non_negative_crash_free_rate_value(data, request, organization)
                 data["query"] = query.query
             except (
                 InvalidParams,

+ 2 - 2
src/sentry/dynamic_sampling/rules/base.py

@@ -48,9 +48,9 @@ def get_guarded_blended_sample_rate(organization: Organization, project: Project
         organization_id=organization.id, project=project
     )
 
-    # If the sample rate is None, it means that dynamic sampling rules shouldn't be generated.
+    # get_blended_sample_rate returns None if the organization doesn't have dynamic sampling
     if sample_rate is None:
-        raise Exception("get_blended_sample_rate returns none")
+        return 1.0
 
     # If the sample rate is 100%, we don't want to use any special dynamic sample rate, we will just sample at 100%.
     if sample_rate == 1.0:

+ 2 - 4
src/sentry/snuba/metrics/span_attribute_extraction.py

@@ -147,10 +147,8 @@ def _map_span_attribute_name(span_attribute: str) -> str:
         return span_attribute
 
     if span_attribute in _SENTRY_TAGS:
-        prefix = "span.sentry_tags"
-    else:
-        prefix = "span.data"
+        return f"span.sentry_tags.{span_attribute}"
 
     sanitized_span_attr = span_attribute.replace(".", "\\.")
 
-    return f"{prefix}.{sanitized_span_attr}"
+    return f"span.data.{sanitized_span_attr}"

+ 57 - 0
src/sentry/utils/metrics.py

@@ -11,7 +11,9 @@ from random import random
 from threading import Thread
 from typing import Any, TypeVar
 
+import sentry_sdk
 from django.conf import settings
+from rest_framework.request import Request
 
 from sentry.metrics.base import MetricsBackend, MutableTags, Tags
 from sentry.metrics.middleware import MiddlewareWrapper, add_global_tags, global_tags
@@ -28,6 +30,7 @@ __all__ = [
     "gauge",
     "backend",
     "MutableTags",
+    "ensure_non_negative_crash_free_rate_value",
 ]
 
 
@@ -259,3 +262,57 @@ def event(
     except Exception:
         logger = logging.getLogger("sentry.errors")
         logger.exception("Unable to record backend metric")
+
+
+def ensure_non_negative_crash_free_rate_value(
+    data: Any, request: Request, organization, CRASH_FREE_RATE_METRIC_KEY="session.crash_free_rate"
+):
+    """
+    Ensures that crash_free_rate metric will never have negative
+    value returned to the customer by replacing all the negative values with 0.
+    Negative value of crash_free_metric can happen due to the
+    corrupted data that is used to calculate the metric
+    (see: https://github.com/getsentry/sentry/issues/73172)
+
+    Example format of data argument:
+    {
+        ...
+        "groups" : [
+            ...
+            "series": {..., "session.crash_free_rate": [..., None, 0.35]},
+            "totals": {..., "session.crash_free_rate": 0.35}
+        ]
+    }
+    """
+    groups = data["groups"]
+    for group in groups:
+        if "series" in group:
+            series = group["series"]
+            if CRASH_FREE_RATE_METRIC_KEY in series:
+                for i, value in enumerate(series[CRASH_FREE_RATE_METRIC_KEY]):
+                    try:
+                        value = float(value)
+                        if value < 0:
+                            with sentry_sdk.push_scope() as scope:
+                                scope.set_tag("organization", organization.id)
+                                scope.set_extra("crash_free_rate_in_series", value)
+                                scope.set_extra("request_query_params", request.query_params)
+                                sentry_sdk.capture_message("crash_free_rate in series is negative")
+                            series[CRASH_FREE_RATE_METRIC_KEY][i] = 0
+                    except TypeError:
+                        # value is not a number
+                        continue
+
+        if "totals" in group:
+            totals = group["totals"]
+            if (
+                CRASH_FREE_RATE_METRIC_KEY in totals
+                and totals[CRASH_FREE_RATE_METRIC_KEY] is not None
+                and totals[CRASH_FREE_RATE_METRIC_KEY] < 0
+            ):
+                with sentry_sdk.push_scope() as scope:
+                    scope.set_tag("organization", organization.id)
+                    scope.set_extra("crash_free_rate", totals[CRASH_FREE_RATE_METRIC_KEY])
+                    scope.set_extra("request_query_params", request.query_params)
+                    sentry_sdk.capture_message("crash_free_rate is negative")
+                totals[CRASH_FREE_RATE_METRIC_KEY] = 0

+ 1 - 8
src/sentry/utils/sdk.py

@@ -76,7 +76,7 @@ SAMPLED_TASKS = {
     "sentry.tasks.derive_code_mappings.derive_code_mappings": settings.SAMPLED_DEFAULT_RATE,
     "sentry.monitors.tasks.clock_pulse": 1.0,
     "sentry.tasks.auto_enable_codecov": settings.SAMPLED_DEFAULT_RATE,
-    "sentry.dynamic_sampling.tasks.boost_low_volume_projects": 1.0,
+    "sentry.dynamic_sampling.tasks.boost_low_volume_projects": 0.2,
     "sentry.dynamic_sampling.tasks.boost_low_volume_transactions": 0.2,
     "sentry.dynamic_sampling.tasks.recalibrate_orgs": 0.2,
     "sentry.dynamic_sampling.tasks.sliding_window_org": 0.2,
@@ -223,13 +223,6 @@ def before_send_transaction(event: Event, _: Hint) -> Event | None:
     ):
         return None
 
-    # This code is added only for debugging purposes, as such, it should be removed once the investigation is done.
-    if event.get("transaction") in {
-        "sentry.dynamic_sampling.boost_low_volume_projects_of_org",
-        "sentry.dynamic_sampling.tasks.boost_low_volume_projects",
-    }:
-        logger.info("transaction_logged", extra=event)
-
     # Occasionally the span limit is hit and we drop spans from transactions, this helps find transactions where this occurs.
     num_of_spans = len(event["spans"])
     event["tags"]["spans_over_limit"] = str(num_of_spans >= 1000)

+ 2 - 1
static/app/types/project.tsx

@@ -23,6 +23,7 @@ export type Project = {
   eventProcessing: {
     symbolicationDegraded: boolean;
   };
+  extrapolateMetrics: boolean;
   features: string[];
   firstEvent: string | null;
   firstTransactionEvent: boolean;
@@ -52,8 +53,8 @@ export type Project = {
   isMember: boolean;
   name: string;
   organization: Organization;
-  plugins: Plugin[];
 
+  plugins: Plugin[];
   processingIssues: number;
   relayCustomMetricCardinalityLimit: number | null;
   relayPiiConfig: string;

+ 4 - 0
static/app/utils/metrics/features.tsx

@@ -17,6 +17,10 @@ export function hasCustomMetricsExtractionRules(organization: Organization) {
   return organization.features.includes('custom-metrics-extraction-rule');
 }
 
+export function hasMetricsExtrapolationFeature(organization: Organization) {
+  return organization.features.includes('metrics-extrapolation');
+}
+
 /**
  * Returns the forceMetricsLayer query param for the alert
  * wrapped in an object so it can be spread into existing query params

+ 46 - 5
static/app/views/settings/projectDebugFiles/index.spec.tsx

@@ -36,19 +36,26 @@ describe('ProjectDebugFiles', function () {
       url: endpoint,
       body: [DebugFileFixture()],
     });
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/builtin-symbol-sources/`,
+      method: 'GET',
+      body: [],
+    });
   });
 
-  it('renders', function () {
+  it('renders', async function () {
     render(<ProjectDebugFiles {...props} />);
 
     expect(screen.getByText('Debug Information Files')).toBeInTheDocument();
 
     // Uploaded debug files content
-    expect(screen.getByText('Uploaded debug information files')).toBeInTheDocument();
+    expect(
+      await screen.findByText('Uploaded debug information files')
+    ).toBeInTheDocument();
     expect(screen.getByText('libS.so')).toBeInTheDocument();
   });
 
-  it('renders empty', function () {
+  it('renders empty', async function () {
     MockApiClient.addMockResponse({
       url: endpoint,
       body: [],
@@ -58,7 +65,7 @@ describe('ProjectDebugFiles', function () {
 
     // Uploaded debug files content
     expect(
-      screen.getByText('There are no debug symbols for this project.')
+      await screen.findByText('There are no debug symbols for this project.')
     ).toBeInTheDocument();
   });
 
@@ -74,7 +81,7 @@ describe('ProjectDebugFiles', function () {
     renderGlobalModal();
 
     // Delete button
-    await userEvent.click(screen.getByTestId('delete-dif'));
+    await userEvent.click(await screen.findByTestId('delete-dif'));
 
     // Confirm Modal
     await screen.findByRole('dialog');
@@ -83,4 +90,38 @@ describe('ProjectDebugFiles', function () {
 
     expect(deleteMock).toHaveBeenCalled();
   });
+
+  it('display error if request for dsyms fails', async function () {
+    MockApiClient.addMockResponse({
+      url: endpoint,
+      body: [DebugFileFixture()],
+      statusCode: 400,
+    });
+
+    render(<ProjectDebugFiles {...props} />);
+
+    expect(await screen.findByText(/There was an error/)).toBeInTheDocument();
+
+    expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
+  });
+
+  it('display error if request for symbol sources fails', async function () {
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/builtin-symbol-sources/`,
+      method: 'GET',
+      body: [],
+      statusCode: 400,
+    });
+
+    render(
+      <ProjectDebugFiles
+        {...props}
+        organization={{...organization, features: ['symbol-sources']}}
+      />
+    );
+
+    expect(await screen.findByText(/There was an error/)).toBeInTheDocument();
+
+    expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
+  });
 });

+ 217 - 189
static/app/views/settings/projectDebugFiles/index.tsx

@@ -1,20 +1,35 @@
-import {Fragment} from 'react';
+import {Fragment, useCallback, useState} from 'react';
 import type {RouteComponentProps} from 'react-router';
 import styled from '@emotion/styled';
 
-import {addErrorMessage} from 'sentry/actionCreators/indicator';
+import {
+  addErrorMessage,
+  addLoadingMessage,
+  addSuccessMessage,
+} from 'sentry/actionCreators/indicator';
 import Checkbox from 'sentry/components/checkbox';
+import LoadingError from 'sentry/components/loadingError';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Pagination from 'sentry/components/pagination';
 import {PanelTable} from 'sentry/components/panels/panelTable';
 import SearchBar from 'sentry/components/searchBar';
+import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
-import ProjectsStore from 'sentry/stores/projectsStore';
 import {space} from 'sentry/styles/space';
 import type {BuiltinSymbolSource, CustomRepo, DebugFile} from 'sentry/types/debugFiles';
 import type {Organization} from 'sentry/types/organization';
 import type {Project} from 'sentry/types/project';
+import {
+  type ApiQueryKey,
+  useApiQuery,
+  useMutation,
+  useQueryClient,
+} from 'sentry/utils/queryClient';
+import {decodeScalar} from 'sentry/utils/queryString';
+import type RequestError from 'sentry/utils/requestError/requestError';
 import routeTitleGen from 'sentry/utils/routeTitle';
-import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
+import useApi from 'sentry/utils/useApi';
+import {useNavigate} from 'sentry/utils/useNavigate';
 import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 import PermissionAlert from 'sentry/views/settings/project/permissionAlert';
@@ -27,158 +42,140 @@ type Props = RouteComponentProps<{projectId: string}, {}> & {
   project: Project;
 };
 
-type State = DeprecatedAsyncView['state'] & {
-  debugFiles: DebugFile[] | null;
-  project: Project;
-  showDetails: boolean;
-  builtinSymbolSources?: BuiltinSymbolSource[] | null;
-};
-
-class ProjectDebugSymbols extends DeprecatedAsyncView<Props, State> {
-  getTitle() {
-    const {projectId} = this.props.params;
-
-    return routeTitleGen(t('Debug Files'), projectId, false);
-  }
-
-  getDefaultState() {
-    return {
-      ...super.getDefaultState(),
-      project: this.props.project,
-      showDetails: false,
-    };
-  }
-
-  getEndpoints(): ReturnType<DeprecatedAsyncView['getEndpoints']> {
-    const {organization, params, location} = this.props;
-    const {builtinSymbolSources} = this.state || {};
-    const {query} = location.query;
-
-    const endpoints: ReturnType<DeprecatedAsyncView['getEndpoints']> = [
-      [
-        'debugFiles',
-        `/projects/${organization.slug}/${params.projectId}/files/dsyms/`,
-        {
-          query: {query},
-        },
-      ],
-    ];
-
-    if (!builtinSymbolSources && organization.features.includes('symbol-sources')) {
-      endpoints.push([
-        'builtinSymbolSources',
-        `/organizations/${organization.slug}/builtin-symbol-sources/`,
-        {},
-      ]);
-    }
-
-    return endpoints;
-  }
-
-  handleDelete = (id: string) => {
-    const {organization, params} = this.props;
-
-    this.setState({
-      loading: true,
-    });
-
-    this.api.request(
-      `/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${id}`,
-      {
-        method: 'DELETE',
-        complete: () => this.fetchData(),
-      }
-    );
-  };
-
-  handleSearch = (query: string) => {
-    const {location, router} = this.props;
-
-    router.push({
-      ...location,
-      query: {...location.query, cursor: undefined, query},
-    });
-  };
-
-  async fetchProject() {
-    const {organization, params} = this.props;
-    try {
-      const updatedProject = await this.api.requestPromise(
-        `/projects/${organization.slug}/${params.projectId}/`
-      );
-      ProjectsStore.onUpdateSuccess(updatedProject);
-    } catch {
-      addErrorMessage(t('An error occurred while fetching project data'));
-    }
-  }
-
-  getQuery() {
-    const {query} = this.props.location.query;
+function makeDebugFilesQueryKey({
+  orgSlug,
+  projectSlug,
+  query,
+}: {
+  orgSlug: string;
+  projectSlug: string;
+  query: string;
+}): ApiQueryKey {
+  return [
+    `/projects/${orgSlug}/${projectSlug}/files/dsyms/`,
+    {
+      query: {query},
+    },
+  ];
+}
 
-    return typeof query === 'string' ? query : undefined;
-  }
+function makeSymbolSourcesQueryKey({orgSlug}: {orgSlug: string}): ApiQueryKey {
+  return [`/organizations/${orgSlug}/builtin-symbol-sources/`];
+}
 
-  getEmptyMessage() {
-    if (this.getQuery()) {
-      return t('There are no debug symbols that match your search.');
+function ProjectDebugSymbols({organization, project, location, router, params}: Props) {
+  const navigate = useNavigate();
+  const api = useApi();
+  const queryClient = useQueryClient();
+  const [showDetails, setShowDetails] = useState(false);
+
+  const query = decodeScalar(location.query.query, '');
+  const hasSymbolSourcesFeatureFlag = organization.features.includes('symbol-sources');
+
+  const {
+    data: debugFiles,
+    getResponseHeader: getDebugFilesResponseHeader,
+    isLoading: isLoadingDebugFiles,
+    isLoadingError: isLoadingErrorDebugFiles,
+    refetch: refetchDebugFiles,
+  } = useApiQuery<DebugFile[] | null>(
+    makeDebugFilesQueryKey({
+      projectSlug: params.projectId,
+      orgSlug: organization.slug,
+      query,
+    }),
+    {
+      staleTime: 0,
+      retry: false,
     }
-
-    return t('There are no debug symbols for this project.');
-  }
-
-  renderLoading() {
-    return this.renderBody();
-  }
-
-  renderDebugFiles() {
-    const {debugFiles, showDetails} = this.state;
-    const {organization, params, project} = this.props;
-
-    if (!debugFiles?.length) {
-      return null;
+  );
+
+  const {
+    data: builtinSymbolSources,
+    isLoading: isLoadingSymbolSources,
+    isError: isErrorSymbolSources,
+    refetch: refetchSymbolSources,
+  } = useApiQuery<BuiltinSymbolSource[] | null>(
+    makeSymbolSourcesQueryKey({orgSlug: organization.slug}),
+    {
+      staleTime: 0,
+      enabled: hasSymbolSourcesFeatureFlag,
+      retry: 0,
     }
-
-    return debugFiles.map(debugFile => {
-      const downloadUrl = `${this.api.baseUrl}/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${debugFile.id}`;
-
-      return (
-        <DebugFileRow
-          debugFile={debugFile}
-          showDetails={showDetails}
-          downloadUrl={downloadUrl}
-          downloadRole={organization.debugFilesRole}
-          onDelete={this.handleDelete}
-          key={debugFile.id}
-          orgSlug={organization.slug}
-          project={project}
-        />
+  );
+
+  const handleSearch = useCallback(
+    (value: string) => {
+      navigate({
+        ...location,
+        query: {...location.query, cursor: undefined, query: !value ? undefined : value},
+      });
+    },
+    [navigate, location]
+  );
+
+  const {mutate: handleDeleteDebugFile} = useMutation<unknown, RequestError, string>({
+    mutationFn: (id: string) => {
+      return api.requestPromise(
+        `/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${id}`,
+        {
+          method: 'DELETE',
+        }
+      );
+    },
+    onMutate: () => {
+      addLoadingMessage('Deleting debug file');
+    },
+    onSuccess: () => {
+      addSuccessMessage('Successfully deleted debug file');
+
+      // invalidate debug files query
+      queryClient.invalidateQueries(
+        makeDebugFilesQueryKey({
+          projectSlug: params.projectId,
+          orgSlug: organization.slug,
+          query,
+        })
       );
-    });
-  }
-
-  renderBody() {
-    const {organization, project, router, location} = this.props;
-    const {loading, showDetails, builtinSymbolSources, debugFiles, debugFilesPageLinks} =
-      this.state;
-
-    return (
-      <Fragment>
-        <SettingsPageHeader title={t('Debug Information Files')} />
-
-        <TextBlock>
-          {t(`
-            Debug information files are used to convert addresses and minified
-            function names from native crash reports into function names and
-            locations.
-          `)}
-        </TextBlock>
-
-        {organization.features.includes('symbol-sources') && (
-          <Fragment>
-            <PermissionAlert project={project} />
 
+      // invalidate symbol sources query
+      queryClient.invalidateQueries(
+        makeSymbolSourcesQueryKey({
+          orgSlug: organization.slug,
+        })
+      );
+    },
+    onError: () => {
+      addErrorMessage('Failed to delete debug file');
+    },
+  });
+
+  return (
+    <SentryDocumentTitle title={routeTitleGen(t('Debug Files'), params.projectId, false)}>
+      <SettingsPageHeader title={t('Debug Information Files')} />
+
+      <TextBlock>
+        {t(`
+          Debug information files are used to convert addresses and minified
+          function names from native crash reports into function names and
+          locations.
+        `)}
+      </TextBlock>
+
+      {organization.features.includes('symbol-sources') && (
+        <Fragment>
+          <PermissionAlert project={project} />
+
+          {isLoadingSymbolSources ? (
+            <LoadingIndicator />
+          ) : isErrorSymbolSources ? (
+            <LoadingError
+              onRetry={refetchSymbolSources}
+              message={t('There was an error loading repositories.')}
+            />
+          ) : (
             <Sources
-              api={this.api}
+              api={api}
               location={location}
               router={router}
               project={project}
@@ -190,48 +187,79 @@ class ProjectDebugSymbols extends DeprecatedAsyncView<Props, State> {
               }
               builtinSymbolSources={project.builtinSymbolSources ?? []}
               builtinSymbolSourceOptions={builtinSymbolSources ?? []}
-              isLoading={loading}
             />
-          </Fragment>
-        )}
-
-        <Wrapper>
-          <TextBlock noMargin>{t('Uploaded debug information files')}</TextBlock>
-          <Filters>
-            <Label>
-              <Checkbox
-                checked={showDetails}
-                onChange={e => {
-                  this.setState({showDetails: (e.target as HTMLInputElement).checked});
-                }}
+          )}
+        </Fragment>
+      )}
+
+      {isLoadingDebugFiles ? (
+        <LoadingIndicator />
+      ) : isLoadingErrorDebugFiles ? (
+        <LoadingError
+          onRetry={refetchDebugFiles}
+          message={t('There was an error loading debug information files.')}
+        />
+      ) : (
+        <Fragment>
+          <Wrapper>
+            <TextBlock noMargin>{t('Uploaded debug information files')}</TextBlock>
+            <Filters>
+              <Label>
+                <Checkbox
+                  checked={showDetails}
+                  onChange={e => {
+                    setShowDetails((e.target as HTMLInputElement).checked);
+                  }}
+                />
+                {t('show details')}
+              </Label>
+
+              <SearchBar
+                placeholder={t('Search DIFs')}
+                onSearch={handleSearch}
+                query={query}
               />
-              {t('show details')}
-            </Label>
-
-            <SearchBar
-              placeholder={t('Search DIFs')}
-              onSearch={this.handleSearch}
-              query={this.getQuery()}
-            />
-          </Filters>
-        </Wrapper>
-
-        <StyledPanelTable
-          headers={[
-            t('Debug ID'),
-            t('Information'),
-            <Actions key="actions">{t('Actions')}</Actions>,
-          ]}
-          emptyMessage={this.getEmptyMessage()}
-          isEmpty={debugFiles?.length === 0}
-          isLoading={loading}
-        >
-          {this.renderDebugFiles()}
-        </StyledPanelTable>
-        <Pagination pageLinks={debugFilesPageLinks} />
-      </Fragment>
-    );
-  }
+            </Filters>
+          </Wrapper>
+
+          <StyledPanelTable
+            headers={[
+              t('Debug ID'),
+              t('Information'),
+              <Actions key="actions">{t('Actions')}</Actions>,
+            ]}
+            emptyMessage={
+              query
+                ? t('There are no debug symbols that match your search.')
+                : t('There are no debug symbols for this project.')
+            }
+            isEmpty={debugFiles?.length === 0}
+            isLoading={isLoadingDebugFiles}
+          >
+            {!debugFiles?.length
+              ? null
+              : debugFiles.map(debugFile => {
+                  const downloadUrl = `${api.baseUrl}/projects/${organization.slug}/${params.projectId}/files/dsyms/?id=${debugFile.id}`;
+
+                  return (
+                    <DebugFileRow
+                      debugFile={debugFile}
+                      showDetails={showDetails}
+                      downloadUrl={downloadUrl}
+                      downloadRole={organization.debugFilesRole}
+                      onDelete={handleDeleteDebugFile}
+                      key={debugFile.id}
+                      orgSlug={organization.slug}
+                      project={project}
+                    />
+                  );
+                })}
+          </StyledPanelTable>
+          <Pagination pageLinks={getDebugFilesResponseHeader?.('Link')} />
+        </Fragment>
+      )}
+    </SentryDocumentTitle>
+  );
 }
 
 const StyledPanelTable = styled(PanelTable)`

+ 0 - 1
static/app/views/settings/projectDebugFiles/sources/builtInRepositories.spec.tsx

@@ -18,7 +18,6 @@ describe('Built-in Repositories', function () {
         api={api}
         organization={organization}
         project={project}
-        isLoading={false}
         builtinSymbolSourceOptions={builtinSymbolSourceOptions}
         builtinSymbolSources={builtinSymbolSources}
       />

Some files were not shown because too many files changed in this diff