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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"@biomejs/biome": "2.3.8",
"@clack/prompts": "^0.11.0",
"@mastra/client-js": "^1.4.0",
"@sentry/api": "^0.94.0",
"@sentry/api": "^0.113.0",
"@sentry/node-core": "10.47.0",
"@sentry/sqlish": "^1.0.0",
"@stricli/auto-complete": "^1.2.4",
Expand Down
6 changes: 3 additions & 3 deletions src/commands/release/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* Create a new Sentry release.
*/

import type { OrgReleaseResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import { createRelease } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
Expand All @@ -19,9 +18,10 @@ import {
import { CommandOutput } from "../../lib/formatters/output.js";
import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../lib/mutate-command.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryRelease } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

function formatReleaseCreated(release: OrgReleaseResponse): string {
function formatReleaseCreated(release: SentryRelease): string {
const lines: string[] = [];
lines.push(`## Release Created: ${escapeMarkdownInline(release.version)}`);
lines.push("");
Expand Down Expand Up @@ -71,7 +71,7 @@ export const createCommand = buildCommand({
" (dry run)"
);
}
return formatReleaseCreated(data as OrgReleaseResponse);
return formatReleaseCreated(data as SentryRelease);
},
},
parameters: {
Expand Down
4 changes: 2 additions & 2 deletions src/commands/release/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* Environment is the first positional arg (required), deploy name is optional second.
*/

import type { DeployResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import { createReleaseDeploy } from "../../lib/api-client.js";
import { buildCommand, numberParser } from "../../lib/command.js";
Expand All @@ -19,6 +18,7 @@ import { CommandOutput } from "../../lib/formatters/output.js";
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../lib/mutate-command.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryDeploy } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

function formatDeployCreated(data: Record<string, unknown>): string {
Expand All @@ -27,7 +27,7 @@ function formatDeployCreated(data: Record<string, unknown>): string {
`Would create deploy for ${safeCodeSpan(String(data.environment))} environment (dry run)`
);
}
const deploy = data as unknown as DeployResponse;
const deploy = data as unknown as SentryDeploy;
const lines: string[] = [];
lines.push("## Deploy Created");
lines.push("");
Expand Down
6 changes: 3 additions & 3 deletions src/commands/release/deploys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* List deploys for a release.
*/

import type { DeployResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import { listReleaseDeploys } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
Expand All @@ -13,9 +12,10 @@ import { CommandOutput } from "../../lib/formatters/output.js";
import { type Column, formatTable } from "../../lib/formatters/table.js";
import { formatRelativeTime } from "../../lib/formatters/time-utils.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryDeploy } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

const DEPLOY_COLUMNS: Column<DeployResponse>[] = [
const DEPLOY_COLUMNS: Column<SentryDeploy>[] = [
{ header: "ENVIRONMENT", value: (d) => d.environment },
{ header: "NAME", value: (d) => d.name || "—" },
{
Expand All @@ -24,7 +24,7 @@ const DEPLOY_COLUMNS: Column<DeployResponse>[] = [
},
];

function formatDeployList(deploys: DeployResponse[]): string {
function formatDeployList(deploys: SentryDeploy[]): string {
if (deploys.length === 0) {
return "No deploys found for this release.";
}
Expand Down
4 changes: 2 additions & 2 deletions src/commands/release/finalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* Finalize a release by setting its dateReleased to now.
*/

import type { OrgReleaseResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import { updateRelease } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
Expand All @@ -19,6 +18,7 @@ import {
import { CommandOutput } from "../../lib/formatters/output.js";
import { DRY_RUN_ALIASES, DRY_RUN_FLAG } from "../../lib/mutate-command.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryRelease } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

function formatReleaseFinalized(data: Record<string, unknown>): string {
Expand All @@ -27,7 +27,7 @@ function formatReleaseFinalized(data: Record<string, unknown>): string {
`Would finalize release ${safeCodeSpan(String(data.version))} (dry run)`
);
}
const release = data as unknown as OrgReleaseResponse;
const release = data as unknown as SentryRelease;
const lines: string[] = [];
lines.push(`## Release Finalized: ${escapeMarkdownInline(release.version)}`);
lines.push("");
Expand Down
10 changes: 5 additions & 5 deletions src/commands/release/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* project scoping, environment filtering, and rich terminal styling.
*/

import type { OrgReleaseResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import {
type ListReleasesOptions,
Expand Down Expand Up @@ -45,11 +44,12 @@ import {
toNumericId,
} from "../../lib/resolve-target.js";
import { buildReleaseUrl } from "../../lib/sentry-urls.js";
import type { SentryRelease } from "../../types/index.js";
import { fmtCrashFree } from "./view.js";

export const PAGINATION_KEY = "release-list";

type ReleaseWithOrg = OrgReleaseResponse & {
type ReleaseWithOrg = SentryRelease & {
orgSlug?: string;
/** Project slug when from multi-project auto-detect (for labeling). */
targetProject?: string;
Expand Down Expand Up @@ -95,7 +95,7 @@ function parseSortFlag(value: string): ReleaseSortValue {
// ---------------------------------------------------------------------------

/** Pick health data from the first project that has it. */
function getHealthData(release: OrgReleaseResponse) {
function getHealthData(release: SentryRelease) {
return release.projects?.find((p) => p.healthData?.hasHealthData)?.healthData;
}

Expand Down Expand Up @@ -156,7 +156,7 @@ function formatAdoption(value: number | null | undefined): string {
}

/** Session sparkline in muted color. */
function formatSessionSparkline(r: OrgReleaseResponse): string {
function formatSessionSparkline(r: SentryRelease): string {
const health = getHealthData(r);
if (!health) {
return "";
Expand Down Expand Up @@ -248,7 +248,7 @@ type ExtraApiOptions = Pick<

function buildReleaseListConfig(
extra: ExtraApiOptions
): OrgListConfig<OrgReleaseResponse, ReleaseWithOrg> {
): OrgListConfig<SentryRelease, ReleaseWithOrg> {
return {
paginationKey: PAGINATION_KEY,
entityName: "release",
Expand Down
10 changes: 5 additions & 5 deletions src/commands/release/set-commits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* Associate commits with a release using auto-discovery or local git history.
*/

import type { OrgReleaseResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import {
setCommitsAuto,
Expand All @@ -30,6 +29,7 @@ import {
} from "../../lib/git.js";
import { logger } from "../../lib/logger.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryRelease } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

const log = logger.withTag("release.set-commits");
Expand All @@ -40,7 +40,7 @@ function setCommitsFromLocal(
version: string,
cwd: string,
depth: number
): Promise<OrgReleaseResponse> {
): Promise<SentryRelease> {
const shallow = isShallowRepository(cwd);
if (shallow) {
log.warn(
Expand Down Expand Up @@ -123,7 +123,7 @@ async function setCommitsDefault(
version: string,
cwd: string,
depth: number
): Promise<OrgReleaseResponse> {
): Promise<SentryRelease> {
// Fast path: cached "no repos" — skip the API call entirely
if (hasNoRepoIntegration(org)) {
return setCommitsFromLocal(org, version, cwd, depth);
Expand Down Expand Up @@ -153,7 +153,7 @@ async function setCommitsDefault(
}
}

function formatCommitsSet(release: OrgReleaseResponse): string {
function formatCommitsSet(release: SentryRelease): string {
const lines: string[] = [];
lines.push(`## Commits Set: ${escapeMarkdownInline(release.version)}`);
lines.push("");
Expand Down Expand Up @@ -310,7 +310,7 @@ export const setCommitsCommand = buildCommand({
return;
}

let release: OrgReleaseResponse;
let release: SentryRelease;

if (flags.local) {
// Explicit --local: use local git only
Expand Down
6 changes: 3 additions & 3 deletions src/commands/release/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* health and adoption metrics when available.
*/

import type { OrgReleaseResponse } from "@sentry/api";
import type { SentryContext } from "../../context.js";
import { getRelease } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
Expand All @@ -28,6 +27,7 @@ import {
FRESH_FLAG,
} from "../../lib/list-command.js";
import { resolveOrg } from "../../lib/resolve-target.js";
import type { SentryRelease } from "../../types/index.js";
import { parseReleaseArg } from "./parse.js";

/** Wrap a plain "—" in muted color for consistent table styling. */
Expand Down Expand Up @@ -56,7 +56,7 @@ export function fmtCrashFree(value: number | null | undefined): string {
* Only includes projects that have health data. Returns empty string
* if no project has data (so the section is skipped entirely).
*/
function formatProjectHealthTable(release: OrgReleaseResponse): string {
function formatProjectHealthTable(release: SentryRelease): string {
const projects = release.projects?.filter((p) => p.healthData?.hasHealthData);
if (!projects?.length) {
return "";
Expand Down Expand Up @@ -94,7 +94,7 @@ function formatProjectHealthTable(release: OrgReleaseResponse): string {
return lines.join("\n");
}

function formatReleaseDetails(release: OrgReleaseResponse): string {
function formatReleaseDetails(release: SentryRelease): string {
const lines: string[] = [];

lines.push(
Expand Down
67 changes: 14 additions & 53 deletions src/lib/api/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* other modules in `src/lib/api/` import from.
*/

import { parseSentryLinkHeader } from "@sentry/api";
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
import type { z } from "zod";
Expand All @@ -19,6 +20,19 @@ import {
getSdkConfig,
} from "../sentry-client.js";

/**
* Parse Sentry's RFC 5988 Link response header to extract pagination cursors.
*
* Sentry Link header format:
* `<url>; rel="next"; results="true"; cursor="1735689600000:0:0"`
*
* Thin alias over `@sentry/api`'s `parseSentryLinkHeader` — the SDK ships the
* canonical parser. We keep the `parseLinkHeader` name because multiple call
* sites import it under that name from the `api-client` barrel and because
* `unwrapPaginatedResult` below needs a local binding.
*/
export const parseLinkHeader = parseSentryLinkHeader;

/** Options for raw API requests to Sentry endpoints. */
export type ApiRequestOptions<T = unknown> = {
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
Expand Down Expand Up @@ -186,29 +200,6 @@ export function buildApiUrl(regionUrl: string, ...segments: string[]): string {
return `${base}/api/0/${path}/`;
}

/**
* Extract the value of a named attribute from a Link header segment.
* Parses `key="value"` pairs using string operations instead of regex
* for robustness and performance.
*
* @param segment - A single Link header segment (e.g., `<url>; rel="next"; cursor="abc"`)
* @param attr - The attribute name to extract (e.g., "rel", "cursor")
* @returns The attribute value, or undefined if not found
*/
function extractLinkAttr(segment: string, attr: string): string | undefined {
const prefix = `${attr}="`;
const start = segment.indexOf(prefix);
if (start === -1) {
return;
}
const valueStart = start + prefix.length;
const end = segment.indexOf('"', valueStart);
if (end === -1) {
return;
}
return segment.slice(valueStart, end);
}

/**
* Maximum number of pages to follow when auto-paginating.
*
Expand Down Expand Up @@ -248,36 +239,6 @@ export type PaginatedResponse<T> = {
nextCursor?: string;
};

/**
* Parse Sentry's RFC 5988 Link response header to extract pagination cursors.
*
* Sentry Link header format:
* `<url>; rel="next"; results="true"; cursor="1735689600000:0:0"`
*
* @param header - Raw Link header string
* @returns Parsed pagination info with next cursor if available
*/
export function parseLinkHeader(header: string | null): {
nextCursor?: string;
} {
if (!header) {
return {};
}

// Split on comma to get individual link entries
for (const part of header.split(",")) {
const rel = extractLinkAttr(part, "rel");
const results = extractLinkAttr(part, "results");
const cursor = extractLinkAttr(part, "cursor");

if (rel === "next" && results === "true" && cursor) {
return { nextCursor: cursor };
}
}

return {};
}

/**
* Make an authenticated request to a specific Sentry region.
* Returns both parsed response data and raw headers for pagination support.
Expand Down
Loading
Loading