-
-
Notifications
You must be signed in to change notification settings - Fork 4.6k
feat(preprod): Add support for Apple insight types on Compare #106967
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,10 @@ | |
| FileSavingsResultGroup, | ||
| FilesInsightResult, | ||
| GroupsInsightResult, | ||
| ImageOptimizationInsightResult, | ||
| OptimizableImageFile, | ||
| StripBinaryFileInfo, | ||
| StripBinaryInsightResult, | ||
| ) | ||
| from sentry.preprod.size_analysis.models import ( | ||
| ComparisonResults, | ||
|
|
@@ -546,6 +550,138 @@ def _compare_groups( | |
| return diff_items | ||
|
|
||
|
|
||
| def _compare_optimizable_images( | ||
| head_files: list[OptimizableImageFile], base_files: list[OptimizableImageFile] | ||
| ) -> list[DiffItem]: | ||
| """Compare optimizable image files between head and base builds.""" | ||
| diff_items = [] | ||
|
|
||
| head_files_dict = {f.file_path: f for f in head_files} | ||
| base_files_dict = {f.file_path: f for f in base_files} | ||
|
|
||
| all_paths = set(head_files_dict.keys()) | set(base_files_dict.keys()) | ||
|
|
||
| for path in sorted(all_paths): | ||
| head_file = head_files_dict.get(path) | ||
| base_file = base_files_dict.get(path) | ||
|
|
||
| if head_file and base_file: | ||
| # File exists in both - check for savings change | ||
| size_diff = head_file.potential_savings - base_file.potential_savings | ||
|
|
||
| # Skip files with no savings change | ||
| if size_diff == 0: | ||
| continue | ||
|
|
||
| diff_type = DiffType.INCREASED if size_diff > 0 else DiffType.DECREASED | ||
|
|
||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=size_diff, | ||
| head_size=head_file.potential_savings, | ||
| base_size=base_file.potential_savings, | ||
| path=path, | ||
| item_type=None, | ||
| type=diff_type, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
| elif head_file: | ||
| # File added in head | ||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=head_file.potential_savings, | ||
| head_size=head_file.potential_savings, | ||
| base_size=None, | ||
| path=path, | ||
| item_type=None, | ||
| type=DiffType.ADDED, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
| elif base_file: | ||
| # File removed from head | ||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=-base_file.potential_savings, | ||
| head_size=None, | ||
| base_size=base_file.potential_savings, | ||
| path=path, | ||
| item_type=None, | ||
| type=DiffType.REMOVED, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
|
|
||
| return diff_items | ||
|
|
||
|
|
||
| def _compare_strip_binary_files( | ||
| head_files: list[StripBinaryFileInfo], base_files: list[StripBinaryFileInfo] | ||
| ) -> list[DiffItem]: | ||
| """Compare strip binary files between head and base builds.""" | ||
| diff_items = [] | ||
|
|
||
| head_files_dict = {f.file_path: f for f in head_files} | ||
| base_files_dict = {f.file_path: f for f in base_files} | ||
|
|
||
| all_paths = set(head_files_dict.keys()) | set(base_files_dict.keys()) | ||
|
|
||
| for path in sorted(all_paths): | ||
| head_file = head_files_dict.get(path) | ||
| base_file = base_files_dict.get(path) | ||
|
|
||
| if head_file and base_file: | ||
| # File exists in both - check for savings change | ||
| size_diff = head_file.total_savings - base_file.total_savings | ||
|
|
||
| # Skip files with no savings change | ||
| if size_diff == 0: | ||
| continue | ||
|
|
||
| diff_type = DiffType.INCREASED if size_diff > 0 else DiffType.DECREASED | ||
|
|
||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=size_diff, | ||
| head_size=head_file.total_savings, | ||
| base_size=base_file.total_savings, | ||
| path=path, | ||
| item_type=None, | ||
| type=diff_type, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
| elif head_file: | ||
| # File added in head | ||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=head_file.total_savings, | ||
| head_size=head_file.total_savings, | ||
| base_size=None, | ||
| path=path, | ||
| item_type=None, | ||
| type=DiffType.ADDED, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
| elif base_file: | ||
| # File removed from head | ||
| diff_items.append( | ||
| DiffItem( | ||
| size_diff=-base_file.total_savings, | ||
| head_size=None, | ||
| base_size=base_file.total_savings, | ||
| path=path, | ||
| item_type=None, | ||
| type=DiffType.REMOVED, | ||
| diff_items=None, | ||
| ) | ||
| ) | ||
|
|
||
| return diff_items | ||
|
|
||
|
|
||
| def _diff_insight( | ||
| insight_type: str, | ||
| head_insight: BaseInsightResult | None, | ||
|
|
@@ -564,6 +700,16 @@ def _diff_insight( | |
| base_files = base_insight.files if isinstance(base_insight, FilesInsightResult) else [] | ||
| head_groups = [] | ||
| base_groups = base_insight.groups if isinstance(base_insight, GroupsInsightResult) else [] | ||
| head_optimizable_images = [] | ||
| base_optimizable_images = ( | ||
| base_insight.optimizable_files | ||
| if isinstance(base_insight, ImageOptimizationInsightResult) | ||
| else [] | ||
| ) | ||
| head_strip_binary_files = [] | ||
| base_strip_binary_files = ( | ||
| base_insight.files if isinstance(base_insight, StripBinaryInsightResult) else [] | ||
| ) | ||
| elif base_insight is None: | ||
| # Should never happen, but here for mypy passing | ||
| assert head_insight is not None | ||
|
|
@@ -574,6 +720,16 @@ def _diff_insight( | |
| base_files = [] | ||
| head_groups = head_insight.groups if isinstance(head_insight, GroupsInsightResult) else [] | ||
| base_groups = [] | ||
| head_optimizable_images = ( | ||
| head_insight.optimizable_files | ||
| if isinstance(head_insight, ImageOptimizationInsightResult) | ||
| else [] | ||
| ) | ||
| base_optimizable_images = [] | ||
| head_strip_binary_files = ( | ||
| head_insight.files if isinstance(head_insight, StripBinaryInsightResult) else [] | ||
| ) | ||
| base_strip_binary_files = [] | ||
| else: | ||
| status = "unresolved" | ||
| # Unresolved insight - compare both | ||
|
|
@@ -582,9 +738,25 @@ def _diff_insight( | |
| base_files = base_insight.files if isinstance(base_insight, FilesInsightResult) else [] | ||
| head_groups = head_insight.groups if isinstance(head_insight, GroupsInsightResult) else [] | ||
| base_groups = base_insight.groups if isinstance(base_insight, GroupsInsightResult) else [] | ||
| head_optimizable_images = ( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be better as a follow up, but all these assignments might be better suited if we instead just check the instance type of the overall insight where our current file/group checks are (line 761). So something more like I know you're just following existing patterns here, so no worries, but could be a nice cleanup now that there's more branching logic
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I saw that the existing pattern wasn't scaling well. Good call. |
||
| head_insight.optimizable_files | ||
| if isinstance(head_insight, ImageOptimizationInsightResult) | ||
| else [] | ||
| ) | ||
| base_optimizable_images = ( | ||
| base_insight.optimizable_files | ||
| if isinstance(base_insight, ImageOptimizationInsightResult) | ||
| else [] | ||
| ) | ||
| head_strip_binary_files = ( | ||
| head_insight.files if isinstance(head_insight, StripBinaryInsightResult) else [] | ||
| ) | ||
| base_strip_binary_files = ( | ||
| base_insight.files if isinstance(base_insight, StripBinaryInsightResult) else [] | ||
| ) | ||
|
|
||
| file_diffs = [] | ||
| group_diffs = [] | ||
| file_diffs: list[DiffItem] = [] | ||
| group_diffs: list[DiffItem] = [] | ||
|
|
||
| if head_files or base_files: | ||
| file_diffs = _compare_files(head_files, base_files) | ||
|
|
@@ -596,8 +768,16 @@ def _diff_insight( | |
| # If no group diffs, we don't want to show an irrelevant insight diff item | ||
| if len(group_diffs) == 0: | ||
| return None | ||
|
|
||
| # TODO(EME-678) Implement non-File/GroupinsightsResult insight diffs in future | ||
| elif head_optimizable_images or base_optimizable_images: | ||
| file_diffs = _compare_optimizable_images(head_optimizable_images, base_optimizable_images) | ||
| # If no file diffs, we don't want to show an irrelevant insight diff item | ||
| if len(file_diffs) == 0: | ||
| return None | ||
| elif head_strip_binary_files or base_strip_binary_files: | ||
| file_diffs = _compare_strip_binary_files(head_strip_binary_files, base_strip_binary_files) | ||
| # If no file diffs, we don't want to show an irrelevant insight diff item | ||
| if len(file_diffs) == 0: | ||
| return None | ||
|
|
||
| return InsightDiffItem( | ||
| insight_type=insight_type, | ||
|
|
@@ -621,11 +801,12 @@ def _compare_insights( | |
| return insight_diff_items | ||
|
|
||
| # Convert insights to dictionaries for easier comparison | ||
| # Use isinstance check to filter out non-insight fields like 'platform' discriminator | ||
| head_insight_dict = ( | ||
| { | ||
| field_name: value | ||
| for field_name, value in vars(head_insights).items() | ||
| if value is not None | ||
| if isinstance(value, BaseInsightResult) | ||
| } | ||
| if head_insights | ||
| else {} | ||
|
|
@@ -634,7 +815,7 @@ def _compare_insights( | |
| { | ||
| field_name: value | ||
| for field_name, value in vars(base_insights).items() | ||
| if value is not None | ||
| if isinstance(value, BaseInsightResult) | ||
| } | ||
| if base_insights | ||
| else {} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from enum import Enum | ||
| from typing import Annotated, Literal | ||
|
|
||
| from pydantic import BaseModel, ConfigDict | ||
| from pydantic import BaseModel, ConfigDict, Field | ||
|
|
||
| from sentry.preprod.models import PreprodArtifactSizeMetrics | ||
| from sentry.preprod.size_analysis.insight_models import ( | ||
|
|
@@ -31,6 +32,7 @@ | |
|
|
||
|
|
||
| class AndroidInsightResults(BaseModel): | ||
| platform: Literal["android"] = "android" | ||
| duplicate_files: DuplicateFilesInsightResult | None = None | ||
| webp_optimization: WebPOptimizationInsightResult | None = None | ||
| large_images: LargeImageFileInsightResult | None = None | ||
|
|
@@ -41,6 +43,7 @@ class AndroidInsightResults(BaseModel): | |
|
|
||
|
|
||
| class AppleInsightResults(BaseModel): | ||
| platform: Literal["apple"] = "apple" | ||
| duplicate_files: DuplicateFilesInsightResult | None = None | ||
| large_images: LargeImageFileInsightResult | None = None | ||
| large_audios: LargeAudioFileInsightResult | None = None | ||
|
|
@@ -134,7 +137,10 @@ class SizeAnalysisResults(BaseModel): | |
| download_size: int | ||
| install_size: int | ||
| treemap: TreemapResults | None = None | ||
| insights: AndroidInsightResults | AppleInsightResults | None = None | ||
| insights: ( | ||
| Annotated[AndroidInsightResults | AppleInsightResults, Field(discriminator="platform")] | ||
| | None | ||
| ) = None | ||
|
Comment on lines
+140
to
+143
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixTo ensure backward compatibility, add Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a valid issue. The class AndroidInsightResults(BaseModel):
platform: Literal["android"] = "android"
class AppleInsightResults(BaseModel):
platform: Literal["apple"] = "apple"When the discriminator field is missing from historical data, Pydantic does not raise a ValidationError. Instead, it falls back to left-to-right union validation and uses the default value. Verified in Django shell: >>> old_data = '{"analysis_duration": 0.5, "insights": {"image_optimization": {"total_savings": 1200, "optimizable_files": []}}}'
>>> result = SizeAnalysisResults.parse_raw(old_data)
>>> result.insights
AndroidInsightResults(platform='android', ...) # Parses successfully, no crashThe behavior is:
This is the intended backwards-compatible behavior documented in the PR. |
||
| analysis_version: str | None = None | ||
| file_analysis: FileAnalysis | None = None | ||
| app_components: list[AppComponent] | None = None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ const FILE_DIFF_INSIGHT_TYPES = [ | |
| 'multiple_native_library_archs', | ||
| 'video_compression', | ||
| 'image_optimization', | ||
| 'strip_binary', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing frontend entry for alternate_icons_optimization insightMedium Severity The backend now generates file-level diffs for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The ticket only talks to |
||
| ]; | ||
|
|
||
| const GROUP_DIFF_INSIGHT_TYPES = ['duplicate_files', 'loose_images']; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Would recommend removing these type of comments, usually they're not adding anything since the function is pretty self-explanatory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point.