Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
push:
branches: [main]
pull_request:

concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
Comment on lines +18 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider pinning Bun version for CI stability.

Using bun-version: latest may cause unexpected CI failures when a new Bun release introduces breaking changes. Consider pinning to a specific version (e.g., 1.2.x) for reproducible builds, or at minimum a major version constraint.

♻️ Suggested fix
       - uses: oven-sh/setup-bun@v2
         with:
-          bun-version: latest
+          bun-version: "1.2"
📝 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
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.2"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/test.yml around lines 18 - 20, Update the GitHub Actions
step that uses oven-sh/setup-bun@v2 to stop using bun-version: latest and
instead pin a stable version or range (for example bun-version: 1.2.x or a
specific patch like 1.2.3) to ensure reproducible CI; locate the step with uses:
oven-sh/setup-bun@v2 and change the bun-version input accordingly so CI does not
automatically float to new, potentially breaking Bun releases.

- run: bun install --frozen-lockfile
- run: bun x tsc --noEmit
- run: bun test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/frontend/dist
/node_modules
/pdb-addr2line/target
scripts/harvested.json
1 change: 0 additions & 1 deletion backend/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export interface FeatureConfig {
revision?: string;
generated_at?: number;
is_pr: boolean;
embedder?: string;
}

export function decodeFeatures([high, low]: EncodedFeatureList, config: FeatureConfig): string[] {
Expand Down
4 changes: 2 additions & 2 deletions backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export default {

remap(str, parsed)
.then(remap => {
return sendToSentry(parsed, remap);
return sendToSentry(parsed, remap, str);
})
.catch(() => {});

Expand Down Expand Up @@ -305,7 +305,7 @@ async function remapAndRedirect(url: URL, parsed_str: string, parsed: Parse, hea
let sentryDetails: { id: string } | { shortId: string; permalink: string } | undefined;

try {
sentryDetails = await sendToSentry(parsed, remapped);
sentryDetails = await sendToSentry(parsed, remapped, parsed_str);
} catch (e) {
console.error("Failed to send to sentry", e);
}
Expand Down
157 changes: 7 additions & 150 deletions backend/remap.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Address, Parse, Remap, ResolvedCommit } from "../lib/parser";
import type { Parse, Remap, ResolvedCommit } from "../lib/parser";
import { getCommit } from "./git";
import { fetchDebugFile } from "./debug-store";
import { getCachedRemap, putCachedRemap } from "./db";
Expand All @@ -7,6 +7,7 @@ import { llvm_symbolizer, pdb_addr2line } from "./system-deps";
import { formatMarkdown } from "./markdown";
import { decodeFeatures } from "./feature";
import { AsyncMutexMap } from "./mutex";
import { adjustBunAddresses, processSymbolizerOutput, filterAddresses } from "./symbolize";

const command_map: { [key: string]: string } = {
I: "AddCommand",
Expand Down Expand Up @@ -63,8 +64,6 @@ export async function remap(parsed_string: string, parse: Parse): Promise<Remap>
return in_progress_remaps.get(key, () => remapUncached(parsed_string, parse));
}

const macho_first_offset = 0x100000000;

export async function remapUncached(
parsed_string: string,
parse: Parse,
Expand Down Expand Up @@ -92,11 +91,9 @@ export async function remapUncached(
throw e;
}

let lines: string[] = [];
let stdout = "";

const bun_addrs = parse.addresses
.filter(a => a.object === "bun")
.map(a => "0x" + (parse.os === "macos" ? macho_first_offset + a.address : a.address).toString(16));
const bun_addrs = adjustBunAddresses(parse.addresses, parse.os);
if (bun_addrs.length > 0) {
const cmd = [
parse.os === "windows" ? pdb_addr2line : llvm_symbolizer,
Expand All @@ -120,58 +117,10 @@ export async function remapUncached(
e.code = "PdbAddr2LineFailed";
}

const stdout = await Bun.readableStreamToText(subproc.stdout);
lines = stdout.split("\n").filter(l => l.length > 0);
stdout = await Bun.readableStreamToText(subproc.stdout);
}

let mapped_addrs: Address[] = parse.addresses.map(addr => {
if (addr.object === "bun") {
const fn_line = lines.shift();
const source_line = lines.shift();
if (fn_line && source_line) {
const parsed_line = parsePdb2AddrLineFile(source_line);

return {
remapped: true,
src: parsed_line
? {
file: parsed_line.file,
line: parsed_line.line,
}
: null,
function: cleanFunctionName(fn_line),
object: "bun",
} satisfies Address;
}
}

return {
remapped: false,
object: addr.object,
address: addr.address,
} satisfies Address;
});

const old = mapped_addrs.slice();
// This appears pretty often, and it does not provide much value
if (mapped_addrs[0]?.function?.includes("WTF::jscSignalHandler")) {
const old = mapped_addrs.slice();

mapped_addrs.shift();

console.log(mapped_addrs);
// remove additional `???` lines
while (mapped_addrs.length > 0 && (!mapped_addrs[0].remapped || mapped_addrs[0].function === "??")) {
mapped_addrs.shift();
}

// if this operation somehow removes all addresses, revert
if (mapped_addrs.length === 0) {
mapped_addrs = old;
}
}

mapped_addrs = filterAddresses(mapped_addrs);
const mapped_addrs = processSymbolizerOutput(parse.addresses, stdout);

const key = parseCacheKey(parse);
let display_version = debug_info.feature_config?.version ?? parse.version;
Expand Down Expand Up @@ -199,7 +148,6 @@ export async function remapUncached(
addresses: mapped_addrs,
command,
features,
embedder: debug_info.feature_config?.embedder,
};
putCachedRemap(key, remap);

Expand All @@ -224,95 +172,4 @@ export async function remapUncached(
return remap;
}

export function filterAddresses(addrs: Address[]): Address[] {
const old = addrs.slice();

while (
addrs[0]?.function?.includes?.("WTF::jscSignalHandler") ||
addrs[0]?.function?.includes?.("assertionFailure") ||
addrs[0]?.function?.includes?.("panic") ||
addrs[0]?.function?.endsWith?.("assert")
) {
addrs.shift();

// remove additional `??` lines
while (addrs.length > 0 && (!addrs[0].remapped || addrs[0].function === "??")) {
addrs.shift();
}
}

// remove trailing ?? lines
while (addrs.length > 0 && (!addrs[addrs.length - 1].remapped || addrs[addrs.length - 1].function === "??")) {
addrs.pop();
}

// if this operation somehow removes all addresses, revert
if (addrs.length === 0) {
return old;
}

return addrs;
}

function withoutZigAnon(str: string): string {
if (str && !str.startsWith("__anon_")) {
// Remove all __anon_${number} patterns
str = str.replace(/__anon_\d+/g, "");
}

if (str && !str.startsWith("__struct_")) {
// Remove all __struct_${number} patterns
str = str.replace(/__struct_\d+/g, "");
}

return str;
}

export function cleanFunctionName(str: string): string {
const last_paren = str.lastIndexOf(")");
if (last_paren === -1) {
return withoutZigAnon(str);
}
let last_open_paren = last_paren;
let n = 1;
while (last_open_paren > 0) {
last_open_paren--;
if (str[last_open_paren] === ")") {
n++;
} else if (str[last_open_paren] === "(") {
n--;
if (n === 0) {
break;
}
}
}
return withoutZigAnon(str.slice(0, last_open_paren).replace(/\(.+?\)/g, "(...)"));
}

export function parsePdb2AddrLineFile(str: string): { file: string; line: number } | null {
if (str.startsWith("??:")) return null;

const last_colon = str.lastIndexOf(":");
if (last_colon === -1) {
return null;
}

const second_colon = str.lastIndexOf(":", last_colon - 1);
if (second_colon === -1) {
return null;
}

const line = Math.floor(Number(str.slice(second_colon + 1, last_colon)));
if (isNaN(line)) {
return null;
}

const file_full = str.slice(0, second_colon).replace(/\\/g, "/");
// Strip the CI build root, keeping the first repo-level dir (src, vendor,
// packages) onward. The old `.*?/src/` regex ate `vendor/libuv/` off paths
// like `.../vendor/libuv/src/win/process.c`.
const m = file_full.match(/(?:^|\/)(src|vendor|packages)\/(.*)$/);
const file = m ? `${m[1]}/${m[2]}` : file_full;

return { file, line };
}
export { filterAddresses, cleanFunctionName, parsePdb2AddrLineFile } from "./symbolize";
32 changes: 17 additions & 15 deletions backend/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { getCodeView } from "./code-view";
const BUN_REPORT_VERSION =
spawnSync(["git", "-C", import.meta.dir, "rev-parse", "--short=9", "HEAD"]).stdout.toString().trim() || "unknown";

async function remapToPayload(parse: Parse, remap: Remap): Promise<Sentry.Payload> {
async function remapToPayload(parse: Parse, remap: Remap, trace_str: string): Promise<Sentry.Payload> {
const event_id = MD5.hash(parse.cache_key!, "hex");
const view_url = `https://bun.report/${trace_str}/view`;
Comment on lines +10 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how trace_str is passed to sendToSentry and whether it's already sanitized
rg -n -A5 'sendToSentry\(' --type=ts

Repository: oven-sh/bun.report

Length of output: 990


🏁 Script executed:

#!/bin/bash
# Check where 'str' and 'parsed_str' come from in backend/index.ts
head -n 185 backend/index.ts | tail -n 10 && echo "---" && head -n 315 backend/index.ts | tail -n 10

Repository: oven-sh/bun.report

Length of output: 602


🏁 Script executed:

#!/bin/bash
# Search for any URL encoding or sanitization utilities in the codebase
rg -n 'encodeURI|encodeURIComponent|sanitize|escape' --type=ts

Repository: oven-sh/bun.report

Length of output: 1735


🏁 Script executed:

#!/bin/bash
# Look for where 'str' is defined/assigned around line 182
sed -n '150,185p' backend/index.ts | head -n 40

Repository: oven-sh/bun.report

Length of output: 1119


🏁 Script executed:

#!/bin/bash
# Look for where 'parsed_str' is defined/assigned around line 308
sed -n '270,310p' backend/index.ts | head -n 50

Repository: oven-sh/bun.report

Length of output: 1240


🏁 Script executed:

#!/bin/bash
# Find where remapAndRedirect is called to understand parsed_str origin
rg -n 'remapAndRedirect' backend/index.ts

Repository: oven-sh/bun.report

Length of output: 242


🏁 Script executed:

#!/bin/bash
# Get more context around line 308 to see where parsed_str is derived
sed -n '260,315p' backend/index.ts

Repository: oven-sh/bun.report

Length of output: 1718


🏁 Script executed:

#!/bin/bash
# Check where str is first assigned/defined before line 200
sed -n '1,200p' backend/index.ts | tail -n 60

Repository: oven-sh/bun.report

Length of output: 1971


Use encodeURI() when constructing the view_url.

The trace_str parameter is derived from the request URL pathname and interpolated directly into the view_url without encoding. While URL pathnames have inherent restrictions, this approach is inconsistent with similar URL construction patterns in backend/index.ts (lines 119, 143), which properly use encodeURI(). To ensure the constructed URL is safe and consistent, wrap trace_str with encodeURI():

Example fix
const view_url = `https://bun.report/${encodeURI(trace_str)}/view`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/sentry.ts` around lines 10 - 12, In remapToPayload, the constructed
view_url uses trace_str raw; update the view_url creation (the view_url variable
inside remapToPayload) to pass trace_str through encodeURI() so the path segment
is properly escaped (e.g., interpolate encodeURI(trace_str) into the URL) to
match other constructors and avoid unsafe characters.


return [
{
Expand All @@ -31,9 +32,9 @@ async function remapToPayload(parse: Parse, remap: Remap): Promise<Sentry.Payloa
dist: buildDist(parse),
level: "fatal",
transaction: remap.command,
tags: getTags(parse, remap),
tags: { ...getTags(parse, remap), ...(view_url.length <= 200 ? { view_url } : {}) },
fingerprint: buildFingerprint(parse, remap),
extra: buildExtra(remap),
extra: buildExtra(remap, view_url),
contexts: {
runtime: {
name: "bun",
Expand All @@ -43,7 +44,7 @@ async function remapToPayload(parse: Parse, remap: Remap): Promise<Sentry.Payloa
device: getOSDeviceContext(parse),
},
timestamp: new Date().getTime() / 1000,
environment: remap.embedder ?? (parse.is_canary ? "canary" : "production"),
environment: parse.is_canary ? "canary" : "production",
sdk: {
integrations: [],
name: "bun-report",
Expand Down Expand Up @@ -78,7 +79,7 @@ function getTags(parse: Parse, remap: Remap): any {
tags.baseline = true;
}

if (!remap.embedder && parse.is_canary) tags.canary = true;
if (parse.is_canary) tags.canary = true;

if (parse.env_flags != null) {
if (parse.env_flags & 0b0001) tags.wsl = true;
Expand Down Expand Up @@ -153,15 +154,16 @@ function getOSContext(parse: Parse): Sentry.OS {
* away. Surfacing it means clicking any crash shows the PR that introduced
* the crashing code (or at least, the PR the build was cut from).
*/
function buildExtra(remap: Remap): Record<string, unknown> | undefined {
function buildExtra(remap: Remap, view_url: string): Record<string, unknown> {
const extra: Record<string, unknown> = { view_url };
const pr = remap.commit.pr;
if (!pr) return undefined;
return {
pr_number: pr.number,
pr_title: pr.title,
pr_branch: pr.ref,
pr_url: `https://github.com/oven-sh/bun/pull/${pr.number}`,
};
if (pr) {
extra.pr_number = pr.number;
extra.pr_title = pr.title;
extra.pr_branch = pr.ref;
extra.pr_url = `https://github.com/oven-sh/bun/pull/${pr.number}`;
}
return extra;
}

function getOSDeviceContext(parse: Parse): Sentry.PayloadEventContexts["device"] {
Expand Down Expand Up @@ -424,12 +426,12 @@ async function fetchEventDetails(eventId: string): Promise<any> {
};
}

export async function sendToSentry(parse: Parse, remap: Remap) {
export async function sendToSentry(parse: Parse, remap: Remap, trace_str: string) {
const url = process.env.SENTRY_DSN;
if (!url) {
return;
}
const event = await remapToPayload(parse, remap);
const event = await remapToPayload(parse, remap, trace_str);
const body = event.map(x => JSON.stringify(x)).join("\n");

console.log(body);
Expand Down
Loading
Loading