Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions src/sentry/preprod/size_analysis/issues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from collections.abc import Iterator
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4

from sentry.issues.grouptype import PreprodDeltaGroupType
from sentry.issues.issue_occurrence import IssueOccurrence


class SizeRegressionOccurrenceBuilder:
def __init__(self):
self.issue_title = None
self.issue_subtitle = None
self.project_id = None

def build(self) -> tuple[IssueOccurrence, dict[str, Any]]:
id = uuid4()
assert self.issue_title is not None, "issue_title must be set"
assert self.issue_title is not None, "issue_subtitle must be set"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wrong variable checked in assertion for issue_subtitle

The assertion on line 19 checks self.issue_title is not None but the error message says "issue_subtitle must be set". This copy-paste error means issue_subtitle is never validated, so it could be None when passed to IssueOccurrence(subtitle=self.issue_subtitle, ...), potentially causing unexpected behavior or errors downstream.

Fix in Cursor Fix in Web

assert self.project_id is not None, "project_id must be set"

current_timestamp = datetime.now(timezone.utc)
id = uuid4().hex
event_id = uuid4().hex

event_data = {
"event_id": event_id,
"platform": "other",
"project_id": self.project_id,
"received": current_timestamp.isoformat(),
"sdk": None,
"tags": {},
"timestamp": current_timestamp.isoformat(),
"environment": "prod",
}

return (
IssueOccurrence(
id=id,
event_id=event_id,
issue_title=self.issue_title,
subtitle=self.issue_subtitle,
project_id=self.project_id,
fingerprint=uuid4().hex,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Fingerprint passed as string instead of list

The fingerprint parameter is passed as a single string uuid4().hex, but IssueOccurrence expects fingerprint to be a Sequence[str] (a list of strings). All other usages in the codebase pass a list like fingerprint=[...]. This type mismatch will cause issues when the fingerprint is processed downstream.

Fix in Cursor Fix in Web

type=PreprodDeltaGroupType,
detection_time=current_timestamp,
level="error",
resource_id="",
evidence_data={
"head_install_size_bytes": self.head_install_size_bytes,
"base_install_size_bytes": self.base_install_size_bytes,
"head_artifact_id": self.head_artifact_id,
"base_artifact_id": self.base_artifact_id,
},
evidence_display={},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Evidence display passed as dict instead of list

The evidence_display parameter is passed as an empty dict {}, but IssueOccurrence expects Sequence[IssueEvidence]. All other usages in the codebase pass an empty list [] when no evidence display is needed. This type mismatch could cause runtime errors when iterating over evidence display.

Fix in Cursor Fix in Web

culprit="",
),
event_data,
)


# from sentry.issues.grouptype import PreprodStaticGroupType
# class PreprodStaticIssueOccurrenceBuilder:
# def __init__(self):
# self.issue_title = None
# self.project_id = None
#
# def build(self) -> tuple[IssueOccurrence, dict[str, Any]]:
# id = uuid4()
# assert self.issue_title is not None, "issue_title must be set"
# assert self.project_id is not None, "project_id must be set"
#
# current_timestamp = datetime.now(timezone.utc)
# id = uuid4().hex
# event_id = uuid4().hex
#
# event_data = {
# "event_id": event_id,
# "platform": "other",
# "project_id": self.project_id,
# "received": current_timestamp.isoformat(),
# "sdk": None,
# "tags": {},
# "timestamp": current_timestamp.isoformat(),
# # "contexts": {"monitor": get_monitor_environment_context(monitor_env)},
# "environment": "prod",
# # "fingerprint": [incident.grouphash],
# }
#
# return (
# IssueOccurrence(
# id=id,
# event_id=event_id,
# issue_title=self.issue_title,
# subtitle="",
# project_id=self.project_id,
# # TODO: fix
# fingerprint=uuid4().hex,
# type=PreprodStaticIssueOccurrenceBuilder,
# # Now?
# detection_time=current_timestamp,
# level="error",
# resource_id="",
# evidence_data={},
# evidence_display={},
# culprit="",
# ),
# event_data,
# )
# def insight_to_occurrences(name: str, insight: dict[str, any]) -> list[SizeIssueOccurrenceBuilder]:
# builder = SizeIssueOccurrenceBuilder()
# builder.issue_title = f"Bad size {name}"
# return [builder]
36 changes: 36 additions & 0 deletions src/sentry/preprod/size_analysis/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.db import router, transaction
from django.utils import timezone

from sentry.issues.producer import PayloadType, produce_occurrence_to_kafka
from sentry.models.files.file import File
from sentry.preprod.models import (
PreprodArtifact,
Expand All @@ -22,6 +23,8 @@
from sentry.utils import metrics
from sentry.utils.json import dumps_htmlsafe

from .issues import SizeRegressionOccurrenceBuilder

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -477,6 +480,39 @@ def _run_size_analysis_comparison(
comparison.state = PreprodArtifactSizeComparison.State.SUCCESS
comparison.save()

################################################################

head_project = head_size_metric.preprod_artifact.project
threshold = head_project.get_option("sentry:preprod_size_issues_delta_install_threshhold_kb")
diff_item = comparison_results.size_metric_diff_item
actual = diff_item.head_install_size - diff_item.base_install_size

logger.warning(
"!!!!!!!!!!!!!!!!? issue threshold: %d actual: %d base: %d head: %d",
threshold,
actual,
diff_item.base_install_size,
diff_item.head_install_size,
)
if actual >= threshold * 1024:
logger.warning("!!!! trigger issue")
builder = SizeRegressionOccurrenceBuilder()
builder.project_id = head_project.id
builder.issue_title = f"Install size regression"
builder.issue_subtitle = f"Size increased {actual // 1024}kb"
builder.head_install_size_bytes = diff_item.head_install_size
builder.base_install_size_bytes = diff_item.base_install_size
builder.base_artifact_id = base_size_metric.preprod_artifact.id
builder.head_artifact_id = head_size_metric.preprod_artifact.id
occurrence, event_data = builder.build()
produce_occurrence_to_kafka(
payload_type=PayloadType.OCCURRENCE,
occurrence=occurrence,
event_data=event_data,
)
logger.warning("!!!! done")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Debug logging statements accidentally committed to production code

Several logger.warning calls with !!!! markers appear to be debug logging statements that were accidentally included. These include messages like "!!!!!!!!!!!!!!!!? issue threshold", "!!!! trigger issue", and "!!!! done" which are not suitable for production logging and will clutter logs.

Fix in Cursor Fix in Web

################################################################

logger.info(
"preprod.size_analysis.compare.success",
extra={"comparison_id": comparison.id},
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/projectoptions/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,6 @@

# Should seer scanner run automatically on new issues
register(key="sentry:seer_scanner_automation", default=True)

register(key="sentry:preprod_size_issues_delta_install_threshhold_kb", default=500)
register(key="sentry:preprod_size_issues_is_16kb_ready", default=True)
5 changes: 5 additions & 0 deletions static/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,11 @@ function buildRoutes(): RouteObject[] {
name: t('PlayStation'),
component: make(() => import('sentry/views/settings/project/tempest')),
},
{
path: 'preprod/',
name: t('Preprod'),
component: make(() => import('sentry/views/settings/project/preprod')),
},
{
path: 'keys/',
name: t('Client Keys'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

import Feature from 'sentry/components/acl/feature';
import {Breadcrumbs, type Crumb} from 'sentry/components/breadcrumbs';
import ConfirmDelete from 'sentry/components/confirmDelete';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import DropdownButton from 'sentry/components/dropdownButton';
import {DropdownMenu, type MenuItemProps} from 'sentry/components/dropdownMenu';
import FeedbackButton from 'sentry/components/feedbackButton/feedbackButton';
Expand All @@ -19,6 +21,7 @@ import {
IconDownload,
IconEllipsis,
IconRefresh,
IconSettings,
IconTelescope,
} from 'sentry/icons';
import {t} from 'sentry/locale';
Expand Down Expand Up @@ -156,6 +159,14 @@ export function BuildDetailsHeaderContent(props: BuildDetailsHeaderContentProps)
{t('Compare Build')}
</Button>
</Link>
<Feature features="organizations:preprod-issues">
<LinkButton
size="sm"
icon={<IconSettings />}
aria-label={t('Settings')}
to={`/settings/${organization.slug}/projects/${projectId}/preprod/`}
/>
</Feature>
<ConfirmDelete
message={t(
'Are you sure you want to delete this build? This action cannot be undone and will permanently remove all associated files and data.'
Expand Down
14 changes: 14 additions & 0 deletions static/app/views/preprod/buildList/buildList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import {Flex} from '@sentry/scraps/layout';

import Feature from 'sentry/components/acl/feature';
import {LinkButton} from 'sentry/components/core/button/linkButton';
import * as Layout from 'sentry/components/layouts/thirds';
import {PreprodBuildsTable} from 'sentry/components/preprod/preprodBuildsTable';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {IconSettings} from 'sentry/icons';
import {t} from 'sentry/locale';
import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
import {decodeScalar} from 'sentry/utils/queryString';
import type RequestError from 'sentry/utils/requestError/requestError';
Expand Down Expand Up @@ -52,6 +56,16 @@ export default function BuildList() {
<Layout.Page>
<Layout.Header>
<Layout.Title>Builds</Layout.Title>
<Layout.HeaderActions>
<Feature features="organizations:preprod-issues">
<LinkButton
size="sm"
icon={<IconSettings />}
aria-label={t('Settings')}
to={`/settings/${organization.slug}/projects/${projectId}/preprod/`}
/>
</Feature>
</Layout.HeaderActions>
</Layout.Header>

<Layout.Body>
Expand Down
7 changes: 7 additions & 0 deletions static/app/views/settings/project/navigationConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export default function getConfiguration({
title: t('PlayStation'),
show: () => !!(organization && hasTempestAccess(organization)) && !isSelfHosted,
},
{
path: `${pathPrefix}/preprod/`,
title: t('Preprod'),
show: () => !!organization?.features?.includes('preprod-issues'),
badge: () => 'beta',
description: t('Size analysis and build distribution configuration.'),
},
],
},
{
Expand Down
93 changes: 93 additions & 0 deletions static/app/views/settings/project/preprod/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {Fragment} from 'react';

import TextBlock from 'sentry/views/settings/components/text/textBlock';
import Feature from 'sentry/components/acl/feature';
import {ButtonBar} from 'sentry/components/core/button/buttonBar';
import FeedbackButton from 'sentry/components/feedbackButton/feedbackButton';
import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
import {t} from 'sentry/locale';
import ProjectsStore from 'sentry/stores/projectsStore';
import SettingsPageHeader from 'sentry/views/settings/components/settingsPageHeader';
import {useProjectSettingsOutlet} from 'sentry/views/settings/project/projectSettingsLayout';
import Access from 'sentry/components/acl/access';
import {ProjectPermissionAlert} from 'sentry/views/settings/project/projectPermissionAlert';
import Form from 'sentry/components/forms/form';
import JsonForm from 'sentry/components/forms/jsonForm';
import useOrganization from 'sentry/utils/useOrganization';
import type {JsonFormObject} from 'sentry/components/forms/types';

const scopeForEdit = 'project:write';


//const defaultInstallSizeAbsoluteDeltaIssueThresholdKb = 500;

export default function PreprodSettings() {
const organization = useOrganization();
const {project} = useProjectSettingsOutlet();

const formGroups: JsonFormObject[] = [
{
title: t('Size Analysis Issues'),
fields: [
{
name: 'sentry:preprod_size_issues_is_16kb_ready',
type: 'boolean',
label: t('Create 16kb ready issues'),
help: t('Toggles whether or not to create issues for 16kb readiness.'),
getData: data => ({options: data}),
},
{
name: 'sentry:preprod_size_issues_delta_install_threshhold_kb',
type: 'number',
label: t('Install size regression threshold (kb)'),
help: t('If there is an regression in install size larger than the threshold an issue will be created.'),
getData: data => ({options: data}),
},
],
},
];

return (
<Fragment>
<Feature features="organizations:preprod-issues" renderDisabled>
<SentryDocumentTitle title={t('Preprod')} />
<SettingsPageHeader
title={t('Preprod')}
action={
<ButtonBar gap="lg">
<FeedbackButton />
</ButtonBar>
}
/>
<Access access={[scopeForEdit]} project={project}>
{({hasAccess}) => (
<Fragment>
<TextBlock>
{t(`
Configure size analysis and build distribution.
`)}
</TextBlock>

<ProjectPermissionAlert access={[scopeForEdit]} project={project} />

<Form
saveOnBlur
apiMethod="PUT"
apiEndpoint={`/projects/${organization.slug}/${project.slug}/`}
initialData={project.options}
onSubmitSuccess={(response) => ProjectsStore.onUpdateSuccess(response)}
>
<JsonForm
disabled={!hasAccess}
features={new Set(organization.features)}
forms={formGroups}
/>
</Form>

</Fragment>
)}
</Access>
</Feature>
</Fragment>
);
}
Loading