Skip to content

feat(error-tracking): scroll a frame's file at the release commit - #107636

Draft
ablaszkiewicz wants to merge 1 commit into
feat/et-repo-path-linksfrom
feat/et-frame-source-file
Draft

ablaszkiewicz wants to merge 1 commit into
feat/et-repo-path-linksfrom
feat/et-frame-source-file

Conversation

@ablaszkiewicz

@ablaszkiewicz ablaszkiewicz commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Merge order

  1. feat(error-tracking): list repository files with git #107631 feat(error-tracking): list repository files with git
  2. feat(error-tracking): store the file list of each release #107632 feat(error-tracking): store the file list of each release
  3. feat(integrations): check that the GitLab token can read the repository #107637 feat(integrations): check that the GitLab token can read the repository
  4. feat(cymbal): add the path resolution service #107633 feat(cymbal): add the path resolution service
  5. feat(cymbal): add repo paths to frames #107634 feat(cymbal): add repo paths to frames
  6. feat(error-tracking): link frames straight to their repo path #107635 feat(error-tracking): link frames straight to their repo path
  7. 👉 feat(error-tracking): scroll a frame's file at the release commit #107636 feat(error-tracking): scroll a frame's file at the release commit

Problem

  • An expanded stack frame shows only the few lines of context that the SDK captured. To read the rest of the function, a person leaves PostHog and opens GitHub.
  • Frames with a repo_path (feat(cymbal): add repo paths to frames #107634) name the exact file and commit, so PostHog can show that file inline.

Changes

  • An expanded in-app frame with a repo_path on GitHub shows its file at the release commit. The box has a fixed height, and it opens centered on the frame line.
  • Scrolling to either edge shows more of the file. It starts with 15 lines on each side and adds 100 per edge. The file loads once, and the window grows in the browser.
  • When the file line differs from the captured code line, the frame keeps the captured context and says so. This guards against a wrong path match or a wrong release.
  • GitHub only for now. GitLab frames keep the captured context.
Before: captured context only After: the file at the release commit
pr7-before pr7-after
  • New endpoint GET /api/projects/:id/error_tracking/git-provider-file-links/source_file/?event_uuid&event_timestamp&frame_raw_id.
  • The server reads the stored event from ClickHouse, and takes the path, line, repository and commit from the stored frame and its release. The client never sends a path or a repository, so a request can only read files that the team's own events name.
  • It fetches through the team's GitHub integration (contents API, 1 MB cap), and caches the file for 24 hours, zstd-compressed.
  • An unavailable file answers 404 with a reason, such as no_integration, rate_limited or file_not_found.
  • Source file reads count in the Prometheus counter error_tracking_repo_paths_source_file_reads_total, by outcome.
  • Mechanical: regenerated OpenAPI types and MCP types, and the test module added to .github/new-events-schema-targets.txt.

Note

The first view of each file costs one GitHub API call on the team's installation. The server cache and Cache-Control: private, max-age=86400 absorb repeat views.

How did you test this code?

  • TestFrameSourceFile catches a read for a frame the stored event does not name, for a frame without a repo_path, and for a release of another team. It also catches a second GitHub fetch for a cached file.
  • sourceFileWindow.test.ts catches off-by-one windows at the file edges and a captured-code check that rejects truncated lines.
  • frameSourceFileLogic.test.ts catches a window that does not grow on scroll, and a lost captured context when the file cannot be read.
  • The SourceFileFromRepository story renders the "after" state above.
  • End to end on a local stack: a Python frame from a private test repository showed its file, and scrolling up revealed the top of the file.
  • Not verified locally: the native-JSON events schema leg. The local ClickHouse test table lacks temporary_properties, so event inserts fail there before the test runs. An already-listed error tracking test fails the same way. CI runs this leg.

👉 Stay up-to-date with PostHog coding conventions for a smoother review.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

The file view appears only on frames with a repo_path, which only teams with the error-tracking-repo-paths flag get.

Automatic notifications

  • Publish to changelog?

Docs update

None. No doc under docs/ covers error tracking source links.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5, 1M context)

  • Session: https://claude.ai/code/session_01DpTwx89x8Wu9Nu9B82mPEj
  • Skills invoked: /improving-drf-endpoints, /adopting-generated-api-types, /writing-kea-logics, /writing-ui-components, /writing-user-facing-copy, /writing-dataclasses, /writing-tests, /writing-code-comments, /writing-pr-descriptions, /stacking-prs.
  • CodeRabbit CLI: skipped. The CLI was signed out, and the person chose to skip setup.
  • The person chose a fixed-height box inside the frame, reads bound to stored events rather than client-sent paths, and GitHub first.
  • The screenshots come from Storybook with invented data. The local end-to-end run used a private repository, so its screenshots stay out of this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DpTwx89x8Wu9Nu9B82mPEj

@ablaszkiewicz ablaszkiewicz self-assigned this Sep 28, 2026
@ablaszkiewicz
ablaszkiewicz added this pull request to stack #107638 September 28, 2026 11:52
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

🚨 Trunk lane — universal lane

This PR is assigned to the universal lane. It cannot merge in parallel with other PRs, so it can take longer to merge. Ask dev-ex if you think this is wrong.

✅ Duplication (Python) — clean

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

⚠️ Bundle size — 🔺 +6.1 KiB (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.95 MiB · 🔺 +6.1 KiB (+0.0%)

File Size Δ vs base
render-query/src/render-query/render-query.js 20.26 MiB 🔺 +4.3 KiB (+0.0%)
posthog-app/src/scenes/AuthenticatedShell.js 248.4 KiB 🔺 +2.0 KiB (+0.8%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

✅ Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.57 MiB · 22 files no change █████████░ 85.5% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.56 MiB · 629 files 🔺 +314 B (+0.0%) █████████░ 88.4% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.38 MiB · 2,332 files 🔺 +7.9 KiB (+0.1%) █████████░ 88.4% of 8.34 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/index.tsx
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.6 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
99.9 KiB src/lib/api.ts
88.4 KiB src/products.tsx
69.4 KiB src/lib/lemon-ui/icons/icons.tsx
40.1 KiB src/lib/utils/eventUsageLogic.ts
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
28.4 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
301.8 KiB ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
271.7 KiB src/taxonomy/core-filter-definitions-by-group.json
267.6 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.7 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
99.9 KiB src/lib/api.ts
98.5 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
88.4 KiB src/products.tsx

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

✅ Toolbar bundle — eager 2.38 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.38 MiB · 19 files no change ████░░░░░░ 41.5% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 44 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
800.0 KiB dist/toolbar/toolbar-app-6DYXIWRU.css
651.4 KiB dist/toolbar/chunk-chunk-TVTQKACM.js
483.6 KiB dist/toolbar/chunk-chunk-6JFSEK3E.js
138.3 KiB dist/toolbar/chunk-chunk-CP4QT72J.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-OHYAIWFE.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-577JVGVT.js
21.0 KiB dist/toolbar/chunk-chunk-GNB7IYR7.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

✅ Dist folder size — 🔺 +194.5 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 946.37 MiB · 🔺 +194.5 KiB (+0.0%)

ℹ️ MCP UI apps size — 33 app(s), 17631.0 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 597.9 KB 196.2 KB
action 454.1 KB 196.2 KB
action-list 564.2 KB 196.2 KB
cohort 453.1 KB 196.2 KB
cohort-list 563.2 KB 196.2 KB
email-template 452.9 KB 196.2 KB
error-details 469.6 KB 196.2 KB
error-issue 453.8 KB 196.2 KB
error-issue-list 564.1 KB 196.2 KB
experiment 561.3 KB 196.2 KB
experiment-list 564.9 KB 196.2 KB
experiment-results 566.3 KB 196.2 KB
feature-flag 566.8 KB 196.2 KB
feature-flag-list 570.5 KB 196.2 KB
feature-flag-testing 457.3 KB 196.2 KB
inline-scan 453.6 KB 196.2 KB
insight-actors 562.3 KB 196.2 KB
invite-email-preview 452.3 KB 196.2 KB
llm-costs 559.3 KB 196.2 KB
session-recording 455.3 KB 196.2 KB
survey 454.7 KB 196.2 KB
survey-global-stats 561.9 KB 196.2 KB
survey-list 564.9 KB 196.2 KB
survey-stats 561.9 KB 196.2 KB
trace-span 453.5 KB 196.2 KB
trace-span-list 564.1 KB 196.2 KB
vision-observation-list 563.3 KB 196.2 KB
workflow 453.4 KB 196.2 KB
workflow-list 563.5 KB 196.2 KB
loops-review 457.8 KB 196.2 KB
query-results 774.1 KB 196.2 KB
render-ui 857.4 KB 196.2 KB
visual-review-snapshots 457.9 KB 196.2 KB

@greptile-apps

greptile-apps Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Medium risk] Adds source file viewing for error stack frames.

This PR is not safe to merge until cached reads honor access revocation and production events provide the required repository path.

Reviews (1) · Last reviewed commit: "feat(error-tracking): scroll through a f..."

Comment on lines +162 to +163
if isinstance(cached, bytes):
return zstd.decompress(cached).decode()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 security Cached source survives access revocation
If a team removes its GitHub integration or loses access to a repository after a file is cached, this branch returns the private source without checking current access. Integration removal and repository-access changes do not clear these cache entries, so the endpoint can keep serving the file for up to 24 hours. Recheck access before returning cached content, or invalidate the entries when access changes.

How this was verified: Cached bytes return before the live integration and repository-access check, and the access-change handlers do not clear this cache.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/error_tracking/backend/logic/repo_files/source_file.py
Line: 162-163

Comment:
**Cached source survives access revocation**
If a team removes its GitHub integration or loses access to a repository after a file is cached, this branch returns the private source without checking current access. Integration removal and repository-access changes do not clear these cache entries, so the endpoint can keep serving the file for up to 24 hours. Recheck access before returning cached content, or invalidate the entries when access changes.

**How this was verified:** Cached bytes return before the live integration and repository-access check, and the access-change handlers do not clear this cache.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

frames = stacktrace.get("frames") if isinstance(stacktrace, dict) else None
for frame in frames if isinstance(frames, list) else []:
if isinstance(frame, dict) and frame.get("raw_id") == frame_raw_id:
repo_path = frame.get("repo_path")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Stored frames lack repo paths
This reads repo_path from the stored exception frame, but the production frame types written to $exception_list do not contain that field. The successful test supplies it by hand, so real processed frames return no_repo_path instead of loading the file. Persist the path in the frame writer before relying on it here, and test with a writer-produced event.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/error_tracking/backend/logic/repo_files/source_file.py
Line: 146

Comment:
**Stored frames lack repo paths**
This reads `repo_path` from the stored exception frame, but the production frame types written to `$exception_list` do not contain that field. The successful test supplies it by hand, so real processed frames return `no_repo_path` instead of loading the file. Persist the path in the frame writer before relying on it here, and test with a writer-produced event.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +182 to +185
except (EgressBudgetExhausted, GitHubRateLimitError):
return _Unavailable(reason="rate_limited")
except GitHubIntegrationError:
return _Unavailable(reason="fetch_failed")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Non-UTF-8 files cause errors
For a source file that is not UTF-8, get_file_entry raises UnicodeDecodeError, which these handlers do not catch. Expanding that frame returns a 500 instead of leaving the captured context in place. Handle the decoding failure as an unavailable file.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/error_tracking/backend/logic/repo_files/source_file.py
Line: 182-185

Comment:
**Non-UTF-8 files cause errors**
For a source file that is not UTF-8, `get_file_entry` raises `UnicodeDecodeError`, which these handlers do not catch. Expanding that frame returns a 500 instead of leaving the captured context in place. Handle the decoding failure as an unavailable file.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The backend now retrieves source files for stored error frames from GitHub at the release commit and exposes them through a read API. The frontend selects eligible frames, loads repository source, checks it against captured code, and displays a scrollable source window with fallback states. Backend and frontend tests and a Storybook story cover the retrieval and display paths.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to b0307

The new inline source view builds GitHub requests from file paths stored in events. It does not encode or validate those paths, so a crafted path can change which GitHub API URL is called with the team's token. Some files can also produce server errors instead of an "unavailable" state. The previously reported typegen and error-handling issues are still open. Resolve these before merging.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to b0307

The new endpoint can return a full repository file to an authorized team user. Its cache can serve a previously fetched file without checking current repository access, and a stored frame path reaches an authenticated GitHub request without file-path validation. Team-scoped lookups and integration checks limit exposure, but revocation and path-handling questions remain.

Retained concerns

  • Medium · security · inferred: A cache hit serves repository source without rechecking current integration access. If removal or deselection does not invalidate the entry, source remains available through the API for the cache lifetime; guaranteed invalidation was not established.
  • Low · security · observed: The new retrieval path passes a stored frame path into an authenticated GitHub contents URL without validating that path. The repository-access check validates the repository identifier, not the appended file path; the exact effect of crafted paths downstream remains uncertain.
Security review details

Security Blast Radius

  • inferred — The independently reachable API is scoped to a team and a stored event, but a successful response exposes the full readable file, not only the lines displayed in the frame. A cache entry is separated by team, repository, commit and path; exposure beyond the checked repository through a crafted path is not established.

Security Findings and Attack Paths

  • observed — The retained path-handling finding concerns a stored frame path flowing to get_file_entry and into the authenticated contents URL without file-path validation. The user cannot pass that path directly in the request, and the repository and GitHub host are constrained.
  • inferred — The deferred authorization concern is a revocation scenario: after a file has been cached, repeated requests can return it without reaching the integration-access check. Whether integration removal or repository deselection reliably invalidates those entries remains unproved.

Trust Boundaries and Controls

  • observed — The action is included in the viewset’s read-action scope. Team-owned release lookup and the integration repository-access check constrain a cache miss; tests cover an unavailable frame and a release outside the team, but do not establish unauthenticated behavior or revocation handling.

Resilience and Maintainability Implications

  • inferred — Cache expiry eventually restores the miss-path access check, but the one-day server and private browser cache policies do not themselves provide immediate access revocation.

Hardening Proposals

  • proposed — Make the cache policy explicit for integration revocation: establish reliable invalidation or recheck current repository access before returning cached source, and align client caching with the intended revocation window.
  • proposed — Validate the stored frame path as a relative repository file path and encode its URL segments before making the authenticated provider request.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes all required sections and clearly explains the problem, user-visible changes, testing, release status, documentation decision, screenshots, agent context, and unverified test …
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (4)
products/error_tracking/backend/logic/repo_files/source_file.py-151-151 (1)

151-151: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate release.id before UUID conversion.

If an event contains a numeric or structured release.id, line 151 passes it to _as_uuid. uuid.UUID can raise AttributeError for these values, but _as_uuid catches only ValueError. The request can therefore fail with 500 instead of returning no_release.

Suggested fix
-                    release_id=release.get("id") if isinstance(release, dict) else None,
+                    release_id=release.get("id")
+                    if isinstance(release, dict) and isinstance(release.get("id"), str)
+                    else None,
products/error_tracking/backend/logic/repo_files/source_file.py-181-185 (1)

181-185: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle non-UTF-8 source files as unavailable.

GitHubIntegration.get_file_entry decodes inline content with UTF-8. A non-UTF-8 file raises UnicodeDecodeError, which _fetch_file does not catch. The exception can escape instead of returning _Unavailable.

Suggested fix
     except (EgressBudgetExhausted, GitHubRateLimitError):
         return _Unavailable(reason="rate_limited")
+    except UnicodeDecodeError:
+        return _Unavailable(reason="fetch_failed")
     except GitHubIntegrationError:
         return _Unavailable(reason="fetch_failed")
frontend/src/lib/components/Errors/Frame/frameSourceFileLogic.ts-110-125 (1)

110-125: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Return null only on 404. Rethrow every other failure.

The bare catch returns null for every error: 5xx responses, network failures, and GitHub rate limits. The UI then shows "The source of this frame isn't available." and reports nothing. Check status === 404 and rethrow any other error, so the loader's failure path and error tracking still run.

frontend/src/lib/components/Errors/Frame/FrameSourceFile.tsx-59-61 (1)

59-61: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Guard showMoreBelow against repeated dispatches.

Each scroll event within EDGE_PX of the bottom dispatches showMoreBelow. The above branch has a guard. This branch has none. The window can therefore grow by several hundred lines during one scroll gesture before React renders the new rows. Add a pending ref, as in the above branch, and clear it when after.length changes.

🧹 Nitpick comments (1)
products/error_tracking/backend/tests/api/test_git_provider_file_links.py (1)

176-192: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Patch installation_can_access_repository in the 404 cases.

The parameterized 404 test does not mock installation_can_access_repository. The test passes today because each case fails before the access check runs. If the resolver order changes, the test can reach the real GitHub call and produce a 404 for a different reason. Mock the access check with return_value=True so that each 404 comes from the stored frame or release.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 3e26aa62-51f8-432d-9b91-d2513fc03d04

📥 Commits

Reviewing files that changed from the base of the PR and between 96433c8 and b72af04.

⛔ Files ignored due to path filters (2)
  • products/error_tracking/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/error_tracking/frontend/generated/api.ts is excluded by !**/generated/**
📒 Files selected for processing (15)
  • .github/new-events-schema-targets.txt
  • frontend/src/lib/components/Errors/Frame/CollapsibleFrame.stories.tsx
  • frontend/src/lib/components/Errors/Frame/CollapsibleFrameContent.tsx
  • frontend/src/lib/components/Errors/Frame/CollapsibleFrameHeader.tsx
  • frontend/src/lib/components/Errors/Frame/FrameSourceFile.tsx
  • frontend/src/lib/components/Errors/Frame/frameSourceFileLogic.test.ts
  • frontend/src/lib/components/Errors/Frame/frameSourceFileLogic.ts
  • frontend/src/lib/components/Errors/Frame/sourceFileWindow.test.ts
  • frontend/src/lib/components/Errors/Frame/sourceFileWindow.ts
  • frontend/src/lib/components/Errors/errorPropertiesLogic.ts
  • products/error_tracking/backend/logic/repo_files/metrics.py
  • products/error_tracking/backend/logic/repo_files/source_file.py
  • products/error_tracking/backend/presentation/views/git_provider_file_link_resolver.py
  • products/error_tracking/backend/tests/api/test_git_provider_file_links.py
  • services/mcp/src/api/generated.ts

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 1 remain after this review.

Comment on lines +76 to +80
fileWindow: (
sourceFile: FrameSourceFileResponseApi | null,
linesAbove: 15,
linesBelow: 15
) => SourceFileWindow | null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Regenerate the kea types. The committed linesAbove/linesBelow types are stale.

Frontend typechecking fails because typegen output differs from the committed file. Typegen produces number for these selector inputs. The committed file declares the literal type 15.

Fix
-            linesAbove: 15,
-            linesBelow: 15
+            linesAbove: number,
+            linesBelow: number
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fileWindow: (
sourceFile: FrameSourceFileResponseApi | null,
linesAbove: 15,
linesBelow: 15
) => SourceFileWindow | null
fileWindow: (
sourceFile: FrameSourceFileResponseApi | null,
linesAbove: number,
linesBelow: number
) => SourceFileWindow | null
🧰 Tools
🪛 GitHub Actions: Frontend CI / Frontend typechecking

[error] 78-79: The schema build command generated changes to this file; git diff --exit-code failed because the working tree is not clean.

Source: Pipeline failures

return f"error_tracking:source_file:v1:{team_id}:{digest}"


def _json(value: Any) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove Any from the JSON boundary.

_json(value: Any) -> Any weakens type checking for values read from the event record. Use object for the input and result, then narrow each decoded shape at its use site. As per coding guidelines: “Write as if mypy --strict were on. Annotate every signature, avoid Any.”

Source: Coding guidelines

},
)
@action(methods=["GET"], detail=False, url_path="source_file")
def source_file(self, request, **kwargs):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Annotate the new view action.

source_file has no parameter or return annotations. Add types for request, **kwargs, and the response. As per coding guidelines: “Write as if mypy --strict were on. Annotate every signature.”

Source: Coding guidelines

@trunk-io

trunk-io Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
Scenes-App/SidePanels SidePanelNotebooks smoke-test The test timed out while waiting for a loading indicator or spinner to disappear. Logs ↗︎
the activity log logic humanizing insights can handle change of insight query as a query wrapped in an InsightVizNode The test failed because the code could not find the 'models.cohortsModel' path in the store. Logs ↗︎
the activity log logic humanizing insights can handle change of a SQL insight query The test failed because it could not find the specified path 'scenes.PreflightCheck.preflightLogic' in the store. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

…mmit

An expanded in-app frame with a repo path shows its file from GitHub at the
release commit in a fixed-height box centered on the frame line, and shows
more lines as you scroll to either edge. The server reads the file path,
line, repository and commit from the stored event and its release, checks
the file against the captured code line, and caches files for a day.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ablaszkiewicz
ablaszkiewicz force-pushed the feat/et-frame-source-file branch from 76a580a to b0307ee Compare September 28, 2026 16:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
products/error_tracking/backend/logic/repo_paths/source_file.py-181-185 (1)

181-185: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle source files that are not UTF-8.

If GitHub returns a source file encoded in another character set, get_file_entry raises UnicodeDecodeError while decoding its bytes. This handler catches only GitHubIntegrationError, so the source-file endpoint returns a 500 instead of an unavailable result. Catch the decoding failure at the retrieval boundary and return a defined unavailable reason. (raw.githubusercontent.com)

🧹 Nitpick comments (1)
products/error_tracking/backend/logic/repo_paths/source_file.py (1)

199-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use object at the JSON boundary instead of Any.

_json narrows its input with isinstance, and its callers narrow the result again. Annotate the parameter and return value as object so later unchecked field access does not bypass type checking. As per coding guidelines: “Write as if mypy --strict were on. Annotate every signature, avoid Any.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: e22201df-95ef-4da5-9071-fca2351fe4e2

📥 Commits

Reviewing files that changed from the base of the PR and between 76a580a and b0307ee.

⛔ Files ignored due to path filters (2)
  • products/error_tracking/frontend/generated/api.schemas.ts is excluded by !**/generated/**
  • products/error_tracking/frontend/generated/api.ts is excluded by !**/generated/**
📒 Files selected for processing (3)
  • products/error_tracking/backend/logic/repo_paths/metrics.py
  • products/error_tracking/backend/logic/repo_paths/source_file.py
  • products/error_tracking/backend/presentation/views/git_provider_file_link_resolver.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.

)
if github is None:
return _Unavailable(reason="no_integration")
entry = github.get_file_entry(repo.slug.path, repo_path, ref=repo.commit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
git diff --no-ext-diff --unified=80 7ba6066981569fa829db03b5159107c52654349b b0307ee482138f47f86d740e75328489d41d429b -- products/error_tracking/backend/logic/repo_paths/source_file.py
printf '\n--- source_file ---\n'
sed -n '1,230p' products/error_tracking/backend/logic/repo_paths/source_file.py
printf '\n--- get_file_entry ---\n'
rg -n -A35 -B12 'def get_file_entry|file_path' posthog/models/github.py posthog/models/github_integration_base.py posthog/egress/github products/error_tracking/backend | head -260
printf '\n--- request helpers ---\n'
rg -n -A35 -B12 'def client_request|urljoin|api.github|repos/' posthog/models/github.py posthog/models/github_integration_base.py posthog/egress/github | head -300

Repository: PostHog/posthog

Length of output: 41582


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- get_file_entry locations ---'
rg -n -l 'get_file_entry|def api_request' posthog products
printf '%s\n' '--- get_file_entry context ---'
rg -n -A45 -B20 'def get_file_entry' posthog products
printf '%s\n' '--- api_request context ---'
rg -n -A80 -B20 'def api_request' posthog/models posthog/egress

Repository: PostHog/posthog

Length of output: 16512


🏁 Script executed:

#!/bin/bash
set -e
sed -n '1,45p' posthog/models/integration/github.py
sed -n '930,978p' posthog/models/integration/github.py

Repository: PostHog/posthog

Length of output: 4245


🏁 Script executed:

#!/bin/bash
set -e
rg -n -A35 -B15 'def _is_safe_github_repo_path|_is_safe_github_repo_path\(' posthog/models/github_integration_base.py posthog/models/integration/github.py posthog products/error_tracking/backend/tests

Repository: PostHog/posthog

Length of output: 42624


Path Traversal

Reachability: External
Exploitability: Moderate
CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Encode and validate file_path before building the GitHub contents URL.

repo_path comes from the stored event frame. get_file_entry inserts it directly into the authenticated request path, and api_request performs no URL encoding or traversal validation. A path containing dot segments can escape the intended contents path when normalized. ? and # can also alter URL parsing. The repository check validates only repo.slug.path.

Suggested fix
diff --git a/posthog/models/integration/github.py b/posthog/models/integration/github.py
--- a/posthog/models/integration/github.py
+++ b/posthog/models/integration/github.py
@@
 import re
 import time
 import base64
 from collections.abc import Mapping
 from dataclasses import dataclass, field
 from datetime import datetime
 from typing import Any
+from urllib.parse import quote
@@
         """
         repo_path = repository if "/" in repository else f"{self.organization()}/{repository}"
+        if file_path.startswith("/") or any(part in {".", ".."} for part in file_path.split("/")):
+            raise GitHubIntegrationError(f"Unsafe file path: {file_path!r}")
+        encoded_file_path = quote(file_path, safe="/")
 
         response = self.api_request(
             "GET",
-            f"/repos/{repo_path}/contents/{file_path}",
+            f"/repos/{repo_path}/contents/{encoded_file_path}",

View in Security blast radius

@posthog

posthog Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

Install the Visual Review Chrome extension to see visual review results at the top of your pull requests.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant