Skip to content

feat(symbolicli): Enable local JS symbolication#1956

Merged
loewenheim merged 10 commits into
masterfrom
sebastian/symbolicli-js-local
May 26, 2026
Merged

feat(symbolicli): Enable local JS symbolication#1956
loewenheim merged 10 commits into
masterfrom
sebastian/symbolicli-js-local

Conversation

@loewenheim
Copy link
Copy Markdown
Contributor

@loewenheim loewenheim commented May 22, 2026

This makes it possible to symbolicate JS events locally with symbolicli by running a pretend Sentry artifact lookup endpoint. Just put some artifact bundles in a directory and call

symbolicli <event.json> --offline --symbols <DIR>

and you should be good. For now it only supports bundles, not individual artifact files.

Closes #1939. Closes SYMBOLI-58.

@loewenheim loewenheim requested a review from a team as a code owner May 22, 2026 16:11
@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 22, 2026

SYMBOLI-58

});
found_bundles.insert(path);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Lookup ignores repeated query parameters

High Severity

The local /lookup handler deserializes debug_id and url as single optional fields, but lookup_js_artifacts sends multiple debug_id and url query pairs in one request. Axum’s standard Query extractor keeps only one value per name, so additional modules’ bundles are never returned and offline JS symbolication can miss artifacts.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9f5329a. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

True in principle, but in practice the lookup function is only ever called with one debug_id/url.

Comment thread crates/symbolicli/src/js_local_source.rs Outdated
out.push(LookupResult::Bundle {
id: path.to_string_lossy().to_string(),
url: index.url(&format!("bundles/{path_encoded}")),
resolved_with: ResolvedWith::Release,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nested bundle download URLs break

Medium Severity

Bundle download URLs encode the full relative zip path (including / as %2F) under /bundles/, while ServeDir serves files from the symbols root. tower-http’s ServeDir does not reliably map such encoded slash paths to nested files, so bundles not at the top level of --symbols can 404 after a successful lookup.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9f5329a. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seems to work fine in my tests (including test_existing_bundle).

Comment thread crates/symbolicli/src/js_local_source.rs Outdated
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This file is just the event module split out of main.rs, with one small fix noted below.

Comment on lines +178 to +179
Sourcemap(JsModule),
Object(RawObjectInfo),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I switched the order here compared to master. Because of serde(untagged) and the fact that RawObjectInfo matches any type, we need to put other variants first.

}
}

mod event {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As mentioned before this module was moved out of this file.

Comment thread crates/symbolicli/src/js_local_source.rs
let mut archive = ZipArchive::new(archive)?;

let manifest = archive.by_name("manifest.json")?;
let manifest: SourceBundleManifest = serde_json::from_reader(manifest)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Invalid zip aborts entire index

Medium Severity

Building the local artifact index walks every .zip under --symbols and returns an error if any file cannot be opened, parsed as a zip, or read as a bundle with manifest.json. One unrelated or corrupt archive in that tree prevents the local lookup server from starting, blocking all offline JS symbolication for the directory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 872a3e1. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I believe this is acceptable.

Comment on lines +38 to +40
let listener = tokio::net::TcpListener::from_std(listener).unwrap();
axum::serve(listener, router).await.unwrap();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The server task spawned in start_server uses .unwrap(). A panic in this task is not propagated, causing silent failures and subsequent connection errors.
Severity: MEDIUM

Suggested Fix

Propagate errors from the spawned task to the caller of start_server. This can be achieved by using a channel (e.g., tokio::sync::oneshot) to communicate the server's startup result back to the main thread. The main thread should wait on this channel to confirm the server has started successfully before proceeding. Replace .unwrap() calls in the task with proper error handling that sends the error over the channel.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: crates/symbolicli/src/js_local_source.rs#L38-L40

Potential issue: The `start_server` function spawns a Tokio task to run a local server
but does not await the task's `JoinHandle`. Inside this spawned task, `unwrap()` is used
after `tokio::net::TcpListener::from_std` and `axum::serve`. If either of these
operations fails, the task will panic. Because the task is not awaited, this panic will
not be propagated to the main thread. The main thread will proceed, assuming the server
is running, and subsequent HTTP requests to the server's URL will fail with connection
errors, making the root cause of the failure difficult to diagnose.

Comment thread crates/symbolicli/src/js_local_source.rs
Comment thread crates/symbolicli/src/js_local_source.rs Outdated
Comment thread crates/symbolicli/README.md Outdated
Co-authored-by: David Herberth <david.herberth@sentry.io>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d5cc6f9. Configure here.

resolved_with: ResolvedWith::Release,
});
found_bundles.insert(path);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Strict dist breaks release lookup

Medium Severity

Release and URL bundle lookup keys include dist with exact equality, but bundles without a dist manifest attribute are indexed with dist: None. When the event carries a dist, symbolication sends that in the lookup query, so those requests fail to match bundles that only have release and URL metadata.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5cc6f9. Configure here.

Comment on lines +232 to +237
.by_url
.get(&ReleaseDistUrl {
release: release.clone(),
dist: dist.clone(),
url: url.clone(),
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The order of variants in the #[serde(untagged)] enum Module has been reversed, which can lead to incorrect deserialization of module data.
Severity: HIGH

Suggested Fix

Revert the order of the variants in the Module enum definition in event.rs to match the previous implementation. The Object(RawObjectInfo) variant should be listed before the Sourcemap(JsModule) variant to ensure correct deserialization priority.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: crates/symbolicli/src/js_local_source.rs#L232-L237

Potential issue: The order of variants in the `#[serde(untagged)]` enum `Module` was
changed, placing `Sourcemap` before `Object`. With untagged enums, `serde` attempts to
deserialize variants in the order they are defined. This change can cause data that
should be deserialized as an `Object(RawObjectInfo)` to be incorrectly parsed as a
`Sourcemap(JsModule)` if the data structure is ambiguous enough to match the `Sourcemap`
variant first. This leads to incorrect data processing and potential downstream failures
when handling module information.

Also affects:

  • crates/symbolicli/src/js_local_source.rs:147~148

@loewenheim loewenheim enabled auto-merge (squash) May 26, 2026 08:34
@loewenheim loewenheim merged commit 55dd169 into master May 26, 2026
28 checks passed
@loewenheim loewenheim deleted the sebastian/symbolicli-js-local branch May 26, 2026 08:46
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.

symbolicli: Enable purely local JS symbolication

2 participants