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
16 changes: 15 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "jsx-a11y"],
"jsPlugins": ["./packages/cli/lint-rules/cli-oxlint-plugin.js"],
"options": {
"typeAware": true
},
"categories": {
"correctness": "error"
},
Expand All @@ -11,10 +14,21 @@
"**/tsconfig*.json",
"packages/toolkit-core/src/generated/**",
"packages/toolkit-core/test/fixtures/**",
"packages/mcp-server-mocks/src/fixtures/**"
"packages/mcp-server-mocks/src/fixtures/**",
"packages/cli/test/init-eval/templates/sveltekit-app/**"
],
"rules": {
"no-unused-vars": "off",
"typescript/return-await": ["error", "always"],
"typescript/await-thenable": "off",
"typescript/no-base-to-string": "off",
"typescript/no-duplicate-type-constituents": "off",
"typescript/no-floating-promises": "off",
"typescript/no-misused-spread": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/require-array-sort-compare": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"jsx-a11y/interactive-supports-focus": "off"
Comment thread
sentry-junior[bot] marked this conversation as resolved.
},
"overrides": [
Expand Down
1 change: 1 addition & 0 deletions apps/local/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"plugins": ["react", "typescript", "oxc"],
"rules": {
"react/rules-of-hooks": "error",
"typescript/return-await": ["error", "always"],
"react/only-export-components": ["warn", { "allowConstantExport": true }]
}
}
2 changes: 1 addition & 1 deletion apps/local/src/App.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function envelope(transaction: string, options: EnvelopeOptions = {}): string {
}

async function startReceiver() {
return tryListen(buildApp(createSpotlightBuffer(10)), 0, "127.0.0.1");
return await tryListen(buildApp(createSpotlightBuffer(10)), 0, "127.0.0.1");
}

async function stopReceiver(
Expand Down
7 changes: 5 additions & 2 deletions apps/local/src/routes/local-workspace-route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ export function LocalWorkspaceRoute({ children }: { children: ReactNode }) {
const workspaceQuery = useWorkspaceQueryState();

const navigateToWorkspace = (view: WorkspaceView) => {
navigate({ pathname: workspacePath(view), search: location.search });
// navigate() can return a promise; `void` marks this as deliberate
// fire-and-forget for no-floating-promises.
void navigate({ pathname: workspacePath(view), search: location.search });
Comment thread
sentry-junior[bot] marked this conversation as resolved.
};

const navigateToCommandSelection = (
Expand All @@ -32,7 +34,8 @@ export function LocalWorkspaceRoute({ children }: { children: ReactNode }) {
search.set("trace", selection.trace);
}
const query = search.toString();
navigate({
// Fire-and-forget, as in navigateToWorkspace.
void navigate({
pathname: workspacePath(view),
search: query ? `?${query}` : "",
});
Expand Down
1 change: 0 additions & 1 deletion apps/local/tsconfig.app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

/* Bundler mode */
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
Expand Down
27 changes: 27 additions & 0 deletions ast-grep-rule-tests/no-async-catch-callback-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
id: no-async-catch-callback
valid:
- |
const value = await getDashboard(org, id).catch((error) => {
throw wrap(error);
});
- |
try {
return await getDashboard(org, id);
} catch (error) {
return await enrichDashboardError(error, ctx);
}
- |
await withDashboardErrors(ctx, () => getDashboard(org, id));
invalid:
- |
const value = await getDashboard(org, id).catch(
async (error: unknown) => await enrichDashboardError(error, ctx),
);
- |
await withProgress(options, fn).catch(async (error) => {
await report(error);
});
- |
run().catch(async function (error) {
await report(error);
});
13 changes: 13 additions & 0 deletions ast-grep-rules/no-async-catch-callback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
id: no-async-catch-callback
language: TypeScript
severity: error
message: Do not pass an async callback to `.catch()`.
note: Use `try { return await op(); } catch (error) { ... }` (or a wrapper such as `withDashboardErrors`) so the error path is awaited explicitly and stays in async stack traces.
rule:
pattern: $PROMISE.catch($HANDLER)
constraints:
HANDLER:
any:
- kind: arrow_function
- kind: function_expression
regex: ^async\b
8 changes: 8 additions & 0 deletions docs/contributing/coding-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ product-specific implementation patterns.
- Root, MCP, and CLI files use Oxlint and Oxfmt. The CLI-specific rules live in
`packages/cli/lint-rules/cli-oxlint-plugin.js`, with inline Oxlint suppressions
for intentional exceptions.
- Oxlint runs type-aware (`options.typeAware` in `.oxlintrc.json`, backed by
`oxlint-tsgolint`). `typescript/return-await` (`always`) is on repo-wide:
write `return await` in async functions so callers stay in async stack
traces. Type-aware correctness rules that still have a backlog are turned
off at the root; #1573 tracks enabling them.
- Don't pass an async callback to `.catch()`. Use `try`/`await`/`catch`, or a
wrapper such as `withDashboardErrors(ctx, fn)`. The `no-async-catch-callback`
ast-grep rule enforces this.
- VS Code uses the Oxc extension for formatting and linting. Run
`pnpm exec oxfmt --write <file>` to format a file.

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"lint-staged": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"simple-git-hooks": "catalog:",
"tsdown": "catalog:",
"tsx": "catalog:",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/.cursor/rules/oxc.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pre-commit hook uses the same tools for staged files.
- Use `const`, Node `node:*` APIs, and argument arrays for child processes.
- Await promises and handle errors at the right boundary. Never use an async
Promise executor or silently discard a failed operation.
- Write `return await` when an async function returns a promise, so the caller
stays in async stack traces. Type-aware `typescript/return-await` enforces it.
- Keep command output in `CommandOutput` and diagnostics in the tagged logger.
- Avoid top-level browser/Node APIs that break supported runtimes.
- Keep tests isolated, with assertions in `it()`/`test()` and no disabled cases.
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ export async function startCli(): Promise<void> {

// Completions are a fast-path (~1ms) — skip .sentryclirc I/O.
if (args[0] === "__complete") {
return runCompletion(args.slice(1)).catch(() => {
return await runCompletion(args.slice(1)).catch(() => {
// Completions should never crash — silently return no results
process.exitCode = 0;
});
Expand All @@ -781,7 +781,7 @@ export async function startCli(): Promise<void> {
// Gracefully degrade: project context is optional.
}

return runCli(args).catch((err) => {
return await runCli(args).catch((err) => {
process.stderr.write(`Fatal: ${redactCredentialText(String(err))}\n`);
process.exitCode = 1;
});
Expand Down
13 changes: 10 additions & 3 deletions packages/cli/src/commands/cli/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,14 @@ async function executeStandardUpgrade(opts: {
const downloadResult = await withProgress(
{ message: `Downloading ${target}...`, json },
async (setMessage) =>
executeUpgrade(method, target, downloadTag, offline, setMessage, source),
await executeUpgrade(
method,
target,
downloadTag,
offline,
setMessage,
source,
),
);

if (downloadResult?.patchBytes) {
Expand Down Expand Up @@ -780,7 +787,7 @@ async function migrateToStandaloneForNightly(opts: {
const downloadResult = await withProgress(
{ message: `Downloading ${target}...`, json },
async (setMessage) =>
executeUpgrade(
await executeUpgrade(
"curl",
target,
downloadTag,
Expand Down Expand Up @@ -1004,7 +1011,7 @@ export const upgradeCommand = buildCommand({
const resolved = await withProgress(
{ message: "Checking for updates...", json: flags.json },
async () =>
resolveTargetWithFallback({
await resolveTargetWithFallback({
resolveOpts: { method, channel, versionArg, channelChanged, flags },
versionArg,
offline: flags.offline,
Expand Down
16 changes: 9 additions & 7 deletions packages/cli/src/commands/dashboard/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { buildDashboardUrl } from "../../lib/sentry-web-urls.js";
import { setOrgProjectContext } from "../../lib/telemetry.js";
import type { DashboardDetail } from "../../types/dashboard.js";
import { enrichDashboardError } from "./resolve.js";
import { withDashboardErrors } from "./resolve.js";

type CreateFlags = {
readonly json: boolean;
Expand Down Expand Up @@ -170,12 +170,14 @@ export const createCommand = buildCommand({
const parsed = parseOrgProjectArg(targetArg);
const { orgSlug, projectIds } = await resolveDashboardTarget(parsed, cwd);

const dashboard = await createDashboard(orgSlug, {
title,
widgets: [],
projects: projectIds.length > 0 ? projectIds : undefined,
}).catch(async (error: unknown) =>
enrichDashboardError(error, { orgSlug, operation: "create" }),
const dashboard = await withDashboardErrors(
{ orgSlug, operation: "create" },
() =>
createDashboard(orgSlug, {
title,
widgets: [],
projects: projectIds.length > 0 ? projectIds : undefined,
}),
);
const url = buildDashboardUrl(orgSlug, dashboard.id);

Expand Down
32 changes: 16 additions & 16 deletions packages/cli/src/commands/dashboard/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import type { DashboardListItem } from "../../types/dashboard.js";
import type { Writer } from "../../types/index.js";
import {
enrichDashboardError,
withDashboardErrors,
parseDashboardListArgs,
resolveOrgFromTarget,
} from "./resolve.js";
Expand Down Expand Up @@ -448,21 +448,21 @@ export const listCommand = buildListCommand("dashboard", {
dashboards: results,
cursorToStore,
allTitles,
} = await withProgress(
{
message: `Fetching dashboards${titleFilter ? ` matching '${titleFilter}'` : ""} (up to ${flags.limit})...`,
json: flags.json,
},
() =>
fetchDashboards(orgSlug, {
limit: flags.limit,
perPage,
serverCursor,
afterId,
glob,
}),
).catch(async (error: unknown) =>
enrichDashboardError(error, { orgSlug, operation: "list" }),
} = await withDashboardErrors({ orgSlug, operation: "list" }, () =>
withProgress(
{
message: `Fetching dashboards${titleFilter ? ` matching '${titleFilter}'` : ""} (up to ${flags.limit})...`,
json: flags.json,
},
() =>
fetchDashboards(orgSlug, {
limit: flags.limit,
perPage,
serverCursor,
afterId,
glob,
}),
),
);

// Advance the pagination cursor stack
Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/commands/dashboard/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,13 @@ export async function resolveDashboardId(
let cursor: string | undefined;

for (let page = 0; page < MAX_PAGINATION_PAGES; page++) {
const { data, nextCursor } = await listDashboardsPaginated(orgSlug, {
perPage: API_MAX_PER_PAGE,
cursor,
}).catch(async (error: unknown) =>
enrichDashboardError(error, { orgSlug, operation: "list" }),
const { data, nextCursor } = await withDashboardErrors(
{ orgSlug, operation: "list" },
() =>
listDashboardsPaginated(orgSlug, {
perPage: API_MAX_PER_PAGE,
cursor,
}),
);
// Match by ID/slug first (e.g. "default-overview"), then fall back to title
const match =
Expand Down Expand Up @@ -788,6 +790,24 @@ export async function enrichDashboardError(
throw error;
}

/**
* Run a dashboard API operation, converting its failures with
* {@link enrichDashboardError}.
*
* @param ctx - Context about the operation for building error messages
* @param fn - The operation to run
*/
export async function withDashboardErrors<T>(
ctx: DashboardErrorContext,
fn: () => Promise<T>,
): Promise<T> {
try {
return await fn();
} catch (error) {
return await enrichDashboardError(error, ctx);
}
}

/**
* User-facing dataset synonyms resolved to the canonical Sentry widget type.
*
Expand Down
19 changes: 12 additions & 7 deletions packages/cli/src/commands/dashboard/restore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { withProgress } from "../../lib/polling.js";
import { buildDashboardUrl } from "../../lib/sentry-web-urls.js";
import type { DashboardDetail } from "../../types/dashboard.js";
import {
enrichDashboardError,
withDashboardErrors,
parseDashboardPositionalArgs,
resolveDashboardId,
resolveOrgFromTarget,
Expand Down Expand Up @@ -106,15 +106,20 @@ export const restoreCommand = buildCommand({
);
const dashboardId = await resolveDashboardId(orgSlug, dashboardRef);

const dashboard = await withProgress(
{ message: `Restoring revision ${flags.revision}...`, json: flags.json },
() => restoreDashboardRevision(orgSlug, dashboardId, flags.revision),
).catch(async (error: unknown) =>
enrichDashboardError(error, {
const dashboard = await withDashboardErrors(
{
orgSlug,
dashboardId,
operation: "update",
}),
},
() =>
withProgress(
{
message: `Restoring revision ${flags.revision}...`,
json: flags.json,
},
() => restoreDashboardRevision(orgSlug, dashboardId, flags.revision),
),
);

const outputData: RestoreResult = {
Expand Down
Loading
Loading