feat(scm-multi-platform-detetion): Extending multi platform detector functionality#117444
Conversation
| # Language → platform mapping (GitHub Linguist names → Sentry base platform IDs) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| GITHUB_LANGUAGE_TO_SENTRY_PLATFORM: dict[str, str] = { |
There was a problem hiding this comment.
Seems that a lot of this is duped relative to src/sentry/integrations/github/platform_detection.py, I think it would be beneficial in terms of understanding the diff, preventing drift, and understanding which aspects are unique to the two variants to centralize some of this.
You could create a third module (e.g. platform_registry.py) holding the shared layer, imported by both:
- Constants/data: GITHUB_LANGUAGE_TO_SENTRY_PLATFORM, IGNORED_LANGUAGES, FRAMEWORKS, derived indexes, _NON_SELECTABLE_PLATFORMS, _PACKAGE_MANIFEST_FILES
- TypedDicts: DetectedPlatform, DetectorRule, FrameworkDef, _PackageManifest
- Core matching: _rule_matches, _framework_matches, _apply_supersession
There was a problem hiding this comment.
Yup, I'll do that real quick
| ) | ||
|
|
||
|
|
||
| def _path_is_ignored(path: str) -> bool: |
There was a problem hiding this comment.
Might not make sense to do @ this stage, but it could be helpful to add tests for some of the new code, would at least tell us we aren't accidentally breaking measurement results via a pure function not working as expected.
Could add tests for: _path_is_ignored, _build_tree_index, _select_active_platforms, _collect_needed_paths, detect_platforms_multi (fake client, a few cases: subdir next.config.js in apps/web/ detected, confidence-aware sort (a small python-django high beats a larger javascript medium), empty tree, truncated: true surfaced).
There was a problem hiding this comment.
Yeah, I'll follow up with a pr with test files for this, I'll add tests for the new branches
| basename = path.rsplit("/", 1)[-1] | ||
|
|
||
| if entry_type == "blob": | ||
| files.add(basename) |
There was a problem hiding this comment.
files is a set of basenames, so a monorepo with 30 package.jsons reports k_reads_needed = 1. The high-read repos are the ones this metric is meant to find, and the dedup makes them all look small. Tier 2 needs full paths to fetch contents anyway, so could store full paths and keep a basename lookup for the existence rules (also gives us subdir attribution later).
There was a problem hiding this comment.
Makes full sense, I updated the code to consider the full paths, pushed
| for fw in _FRAMEWORKS_BY_PLATFORM.get(base_platform, []): | ||
| # Pass empty file_contents and no manifest so only path/dir/ext | ||
| # existence rules fire; content/package rules return False here. | ||
| if _framework_matches(fw, index.files, {}, None, index.dirs): |
There was a problem hiding this comment.
Whole-tree basename matching loses co-location, so every rules can be satisfied across unrelated subprojects: a stray .csproj in tools/ plus an appsettings.json anywhere reads as dotnet-aspnetcore high, and the ignore list can't catch it. Non-blocking for measurement, but worth a test repo with that shape.
There was a problem hiding this comment.
Yup, makes sense, I've updated the local plan file I have to track the project. Will take into account when implementing the next stage
| platform=platform_id, | ||
| language=language, | ||
| bytes=byte_count, | ||
| confidence="high", |
There was a problem hiding this comment.
Package/content-only frameworks can't match here, so existence siblings win: an RN repo comes back apple-ios high via ios/*.xcodeproj while react-native is invisible. Nothing to change in this PR, but once Tier 2 reads land, escalation shouldn't stop on a high match while needed_paths is non-empty.
There was a problem hiding this comment.
Makes sense, updated my local impl plan file with this comment
| # languages (e.g. TS after JS) are grouped for free. | ||
| count += 1 | ||
| if count > MAX_LANGUAGES: | ||
| break |
There was a problem hiding this comment.
Should this be continue? Once a 4th platform shows up, later related languages stop grouping (byte order TS, Python, Go, C#, JS: the JS bytes never join the javascript bucket), which skews the bytes sort.
There was a problem hiding this comment.
yeah, makes sense, changed to continue, pushed
|
|
||
| if entry_type == "blob": | ||
| files.add(basename) | ||
| repo_size_bytes += entry.get("size") or 0 |
There was a problem hiding this comment.
This used to sum every blob, now only non-ignored ones, so the existing series quietly changes meaning. Full size is also what the Tier 4 archive decision needs (the tarball includes vendored dirs). Maybe emit both?
There was a problem hiding this comment.
Yep, emitting full_repo_size_bytes now fwiw
| count += 1 | ||
| if count > MAX_LANGUAGES: | ||
| continue | ||
| active_platforms[base_platform].append((language, byte_count)) |
There was a problem hiding this comment.
Fourth base platform dropped entirely
Medium Severity
_select_active_platforms skips any language whose base platform is not already active once three distinct base platforms have been counted. If JavaScript is the first language mapped to javascript but appears only after three other base platforms, its bytes are never added and that stack is omitted from detection entirely.
Reviewed by Cursor Bugbot for commit b5f90fa. Configure here.
There was a problem hiding this comment.
break -> continue fixed this
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3103f3e. Configure here.
| if entry_type == "blob": | ||
| full_paths_by_basename[basename].add(path) | ||
| elif entry_type == "tree": | ||
| dirs.add(basename) |
There was a problem hiding this comment.
Unity dirs need same project root
Medium Severity
_build_tree_index stores directory names as a flat set of basenames, and tier‑1 framework matching passes that set into _framework_matches. Unity’s every rule only checks that Assets and ProjectSettings exist somewhere in the repo, not under the same project root, so unrelated folders with those names can yield a false high‑confidence Unity hit in detect_platforms_multi.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3103f3e. Configure here.
There was a problem hiding this comment.
Yeah, taken into account
| for fw in _FRAMEWORKS_BY_PLATFORM.get(base_platform, []): | ||
| # Pass empty file_contents and no manifest so only path/dir/ext | ||
| # existence rules fire; content/package rules return False here. | ||
| if _framework_matches(fw, index.files, {}, None, index.dirs): |
There was a problem hiding this comment.
index.files builds a new set from all basenames on every access, and this loop reads it once per framework (35 times for a javascript bucket). Computing it once into a local before pass 1 avoids rebuilding it against a potentially 100k entry tree.
There was a problem hiding this comment.
Yup, will add this change


What's novel:
Top-N language selection —
_select_active_platforms(L1078) The old detector picks one language. This selects up to MAX_LANGUAGES=3 distinct base platforms by byte count, grouping related languages (TypeScript + JavaScript → javascript). It's what enables detecting a Next.js frontend and Django backend in the same run.Recursive tree fetch and index —
_get_tree,_build_tree_index,_TreeIndex(L1187–L1249) The old detector only looks at the root listing. These fetch the full recursive git tree once and index every file and directory basename across the whole repo, making framework config files detectable anywhere in the tree — not just at root.Noise-scoping ignore list —
_IGNORED_TREE_SEGMENTS,_path_is_ignored(L1123–L1184) A whole-tree index introduces false positives: node_modules/express/package.json would contribute express as a basename signal. The ignore list drops any tree entry whose path passes through a vendored or build directory, which the root-only detector never needed.K-reads accounting —
_collect_needed_paths(L1270) Instead of fetching content eagerly, Tier 1 skips all reads and counts what would be needed to resolve content/package rules. This is emitted as k_reads_needed and is the key input for sizing the Tier 2 read budget before committing to it.Confidence-aware sort —
_CONFIDENCE_ORDER(L1044) With multiple base platforms in one run, a bare-language medium fallback (e.g. javascript) could outsort a high-confidence framework from a smaller language (e.g. python-django) purely on byte count. Adding confidence as the primary sort key prevents that.MultiDetectionResult (L1252) Wraps the platform list with measurement scaffolding — k_candidate, is_truncated, tree_entry_count, repo_size_bytes — that feeds the harness and metrics without leaking into the return type of the eventual live detector. This is just used by the test script. These fields are explicitly temporary and will be stripped once thresholds are set.