Skip to content

Commit 3024d32

Browse files
authored
feat: Extract and send tooling versions to Sentry (EME-606) (#464)
1 parent ab66725 commit 3024d32

File tree

9 files changed

+161
-2
lines changed

9 files changed

+161
-2
lines changed

src/launchpad/api/update_api_models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ class PutSizePending(BaseModel):
3737
]
3838

3939

40-
class AppleAppInfo(BaseModel):
40+
class BaseAppInfo(BaseModel):
41+
cli_version: Optional[str] = Field(None, description="sentry-cli version used for uploading")
42+
43+
44+
class AppleAppInfo(BaseAppInfo):
4145
is_simulator: bool
4246
codesigning_type: Optional[str] = None
4347
profile_name: Optional[str] = None
@@ -48,10 +52,12 @@ class AppleAppInfo(BaseModel):
4852
certificate_expiration_date: Optional[str] = None
4953
missing_dsym_binaries: Optional[List[str]] = None
5054
build_date: Optional[str] = None
55+
fastlane_plugin_version: Optional[str] = Field(None, description="Fastlane plugin version used for uploading")
5156

5257

53-
class AndroidAppInfo(BaseModel):
58+
class AndroidAppInfo(BaseAppInfo):
5459
has_proguard_mapping: bool
60+
gradle_plugin_version: Optional[str] = Field(None, description="Gradle plugin version used for uploading")
5561

5662

5763
class UpdateData(BaseModel):

src/launchpad/artifact_processor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,12 +492,16 @@ def _get_artifact_type(artifact: Artifact) -> ArtifactType:
492492
certificate_expiration_date=app_info.certificate_expiration_date,
493493
missing_dsym_binaries=app_info.missing_dsym_binaries,
494494
build_date=app_info.build_date,
495+
cli_version=app_info.cli_version,
496+
fastlane_plugin_version=app_info.fastlane_plugin_version,
495497
)
496498

497499
android_app_info = None
498500
if isinstance(app_info, AndroidAppInfo):
499501
android_app_info = AndroidAppInfoModel(
500502
has_proguard_mapping=app_info.has_proguard_mapping,
503+
cli_version=app_info.cli_version,
504+
gradle_plugin_version=app_info.gradle_plugin_version,
501505
)
502506

503507
update_data = UpdateData(

src/launchpad/size/analyzers/android.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from launchpad.size.utils.android_bundle_size import calculate_apk_download_size, calculate_apk_install_size
2828
from launchpad.utils.file_utils import calculate_file_hash
2929
from launchpad.utils.logging import get_logger
30+
from launchpad.utils.metadata_extractor import extract_metadata_from_zip
3031

3132
logger = get_logger(__name__)
3233

@@ -50,12 +51,16 @@ def preprocess(self, artifact: AndroidArtifact) -> AndroidAppInfo:
5051
manifest_dict = artifact.get_manifest().model_dump()
5152
has_proguard_mapping = artifact.get_dex_mapping() is not None
5253

54+
metadata = extract_metadata_from_zip(artifact.path)
55+
5356
self.app_info = AndroidAppInfo(
5457
name=manifest_dict["application"]["label"] or "Unknown",
5558
version=manifest_dict["version_name"] or "Unknown",
5659
build=manifest_dict["version_code"] or "Unknown",
5760
app_id=manifest_dict["package_name"],
5861
has_proguard_mapping=has_proguard_mapping,
62+
cli_version=metadata.cli_version,
63+
gradle_plugin_version=metadata.gradle_plugin_version,
5964
)
6065

6166
return self.app_info

src/launchpad/size/analyzers/apple.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from launchpad.utils.apple.code_signature_validator import CodeSignatureValidator
4545
from launchpad.utils.file_utils import get_file_size, to_nearest_block_size
4646
from launchpad.utils.logging import get_logger
47+
from launchpad.utils.metadata_extractor import extract_metadata_from_zip
4748

4849
from ..models.apple import (
4950
AppleAnalysisResults,
@@ -333,6 +334,8 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
333334
binaries = xcarchive.get_all_binary_paths()
334335
missing_dsym_binaries = [b.name for b in binaries if b.dsym_path is None]
335336

337+
metadata = extract_metadata_from_zip(xcarchive.path)
338+
336339
return AppleAppInfo(
337340
name=app_name,
338341
app_id=plist.get("CFBundleIdentifier", "unknown.bundle.id"),
@@ -354,6 +357,8 @@ def _extract_app_info(self, xcarchive: ZippedXCArchive) -> AppleAppInfo:
354357
primary_icon_name=primary_icon_name,
355358
alternate_icon_names=alternate_icon_names,
356359
missing_dsym_binaries=missing_dsym_binaries,
360+
cli_version=metadata.cli_version,
361+
fastlane_plugin_version=metadata.fastlane_plugin_version,
357362
)
358363

359364
def _get_profile_type(self, profile_data: dict[str, Any]) -> Tuple[str, str]:

src/launchpad/size/models/android.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class AndroidAppInfo(BaseAppInfo):
3030
model_config = ConfigDict(frozen=True)
3131

3232
has_proguard_mapping: bool = Field(default=False, description="Whether the app has a proguard mapping file")
33+
gradle_plugin_version: str | None = Field(None, description="Gradle plugin version used for uploading")
3334

3435

3536
class AndroidAnalysisResults(BaseAnalysisResults):

src/launchpad/size/models/apple.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class AppleAppInfo(BaseAppInfo):
7070
missing_dsym_binaries: List[str] = Field(
7171
default_factory=list, description="List of binary names that don't have corresponding dSYM files"
7272
)
73+
fastlane_plugin_version: str | None = Field(None, description="Fastlane plugin version used for uploading")
7374

7475

7576
class AppleInsightResults(BaseModel):

src/launchpad/size/models/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class BaseAppInfo(BaseModel):
2828
version: str = Field(..., description="App version")
2929
build: str = Field(..., description="Build number")
3030
app_id: str = Field(..., description="App ID (bundle id on iOS, package name on Android)")
31+
cli_version: str | None = Field(None, description="sentry-cli version used for uploading")
3132

3233

3334
class FileAnalysis(BaseModel):
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import zipfile
2+
3+
from pathlib import Path
4+
from typing import Dict, Optional
5+
6+
from launchpad.utils.logging import get_logger
7+
8+
logger = get_logger(__name__)
9+
10+
METADATA_FILENAME = ".sentry-cli-metadata.txt"
11+
12+
13+
class ToolingMetadata:
14+
def __init__(
15+
self,
16+
cli_version: Optional[str] = None,
17+
fastlane_plugin_version: Optional[str] = None,
18+
gradle_plugin_version: Optional[str] = None,
19+
):
20+
self.cli_version = cli_version
21+
self.fastlane_plugin_version = fastlane_plugin_version
22+
self.gradle_plugin_version = gradle_plugin_version
23+
24+
25+
def extract_metadata_from_zip(zip_path: Path) -> ToolingMetadata:
26+
try:
27+
with zipfile.ZipFile(zip_path, "r") as zf:
28+
# Only look for .sentry-cli-metadata.txt in the root of the zip
29+
if METADATA_FILENAME not in zf.namelist():
30+
logger.debug(f"No {METADATA_FILENAME} found in root of {zip_path}")
31+
return ToolingMetadata()
32+
33+
logger.debug(f"Found metadata file: {METADATA_FILENAME}")
34+
35+
with zf.open(METADATA_FILENAME) as f:
36+
content = f.read().decode("utf-8")
37+
return _parse_metadata_content(content)
38+
39+
except Exception as e:
40+
logger.warning(f"Failed to extract metadata from {zip_path}: {e}")
41+
return ToolingMetadata()
42+
43+
44+
def _parse_metadata_content(content: str) -> ToolingMetadata:
45+
"""Expected format:
46+
sentry-cli-version: 2.58.2
47+
sentry-fastlane-plugin: 1.2.3
48+
sentry-gradle-plugin: 4.12.0
49+
"""
50+
metadata: Dict[str, str] = {}
51+
52+
for line in content.strip().split("\n"):
53+
line = line.strip()
54+
if not line or ":" not in line:
55+
continue
56+
57+
key, value = line.split(":", 1)
58+
key = key.strip()
59+
value = value.strip()
60+
61+
metadata[key] = value
62+
63+
return ToolingMetadata(
64+
cli_version=metadata.get("sentry-cli-version"),
65+
fastlane_plugin_version=metadata.get("sentry-fastlane-plugin"),
66+
gradle_plugin_version=metadata.get("sentry-gradle-plugin"),
67+
)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import tempfile
2+
import zipfile
3+
4+
from pathlib import Path
5+
6+
from launchpad.utils.metadata_extractor import (
7+
ToolingMetadata,
8+
_parse_metadata_content,
9+
extract_metadata_from_zip,
10+
)
11+
12+
13+
class TestParseMetadataContent:
14+
def test_parse_all_fields(self):
15+
content = """sentry-cli-version: 2.58.2
16+
sentry-fastlane-plugin: 1.2.3
17+
sentry-gradle-plugin: 4.12.0"""
18+
metadata = _parse_metadata_content(content)
19+
assert metadata.cli_version == "2.58.2"
20+
assert metadata.fastlane_plugin_version == "1.2.3"
21+
assert metadata.gradle_plugin_version == "4.12.0"
22+
23+
def test_parse_empty_content(self):
24+
content = ""
25+
metadata = _parse_metadata_content(content)
26+
assert metadata.cli_version is None
27+
assert metadata.fastlane_plugin_version is None
28+
assert metadata.gradle_plugin_version is None
29+
30+
31+
class TestExtractMetadataFromZip:
32+
def test_extract_from_zip_root(self):
33+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
34+
try:
35+
with zipfile.ZipFile(tf.name, "w") as zf:
36+
zf.writestr(
37+
".sentry-cli-metadata.txt",
38+
"sentry-cli-version: 2.58.2\nsentry-fastlane-plugin: 1.2.3\nsentry-gradle-plugin: 4.12.0",
39+
)
40+
zf.writestr("some-file.txt", "content")
41+
42+
metadata = extract_metadata_from_zip(Path(tf.name))
43+
assert metadata.cli_version == "2.58.2"
44+
assert metadata.fastlane_plugin_version == "1.2.3"
45+
assert metadata.gradle_plugin_version == "4.12.0"
46+
finally:
47+
Path(tf.name).unlink()
48+
49+
def test_extract_when_missing(self):
50+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tf:
51+
try:
52+
with zipfile.ZipFile(tf.name, "w") as zf:
53+
zf.writestr("some-file.txt", "content")
54+
zf.writestr("other-file.txt", "content")
55+
56+
metadata = extract_metadata_from_zip(Path(tf.name))
57+
assert metadata.cli_version is None
58+
assert metadata.fastlane_plugin_version is None
59+
assert metadata.gradle_plugin_version is None
60+
finally:
61+
Path(tf.name).unlink()
62+
63+
64+
class TestToolingMetadata:
65+
def test_create_with_defaults(self):
66+
metadata = ToolingMetadata()
67+
assert metadata.cli_version is None
68+
assert metadata.fastlane_plugin_version is None
69+
assert metadata.gradle_plugin_version is None

0 commit comments

Comments
 (0)