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
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,22 @@ View details of one or more events
| `message` | string \| null | Event message |
| `title` | string | Event title |
| `location` | string \| null | Source location of the event, such as a file and line |
| `user` | object \| null | User context of the event |
| `tags` | array | Tags attached to the event |
| `platform` | string \| null | Platform of the event, such as python or javascript |
| `dateReceived` | string \| null | When Sentry received the event (ISO 8601) |
| `contexts` | object \| null | Contexts of the event, such as device, os, browser and runtime |
| `size` | number \| null | Size of the event payload in bytes |
| `entries` | array | Event interfaces, such as exception, breadcrumbs and request, each with a type and data |
| `dist` | string \| null | Distribution of the release the event was sent with |
| `sdk` | object \| null | SDK that sent the event |
| `context` | object \| null | Additional data sent with the event in its extra field |
| `packages` | object | Modules loaded when the event occurred, keyed by module name with their versions |
| `type` | string | Event type, such as error, default or transaction |
| `metadata` | object | Event metadata |
| `errors` | array | Errors Sentry found while processing the event |
| `occurrence` | object \| null | |
| `_meta` | object | Metadata about changes Sentry made to the event's fields, such as data scrubbing or trimming |
| `user` | object \| null | User context of the event |
| `tags` | array | Tags attached to the event |
| `contexts` | object \| null | Contexts of the event, such as device, os, browser and runtime |
| `sdk` | object \| null | SDK that sent the event |
| `context` | object \| null | Additional data sent with the event in its extra field |
| `errors` | array | Errors Sentry found while processing the event |
| `crashFile` | string \| null | Crash report attachment of the event, if any |
| `culprit` | string \| null | Function or module that caused the event |
| `dateCreated` | string | When the event occurred (ISO 8601) |
Expand Down
84 changes: 49 additions & 35 deletions packages/cli/src/lib/api/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
} from "@sentry/api";
import {
getOrganizationIssue,
getOrganizationSharedIssue,
listOrganizationIssues,
resolveOrganizationShortId,
updateOrganizationIssue,
Expand Down Expand Up @@ -727,30 +728,62 @@ export async function getSharedIssue(
shareId: string,
): Promise<{ id: string }> {
const path = `organizations/${encodeURIComponent(orgSlug)}/shared/issues/${encodeURIComponent(shareId)}/`;
const url = `${baseUrl.replace(TRAILING_SLASH_RE, "")}/api/0/${path}`;
const normalizedBase = baseUrl.replace(TRAILING_SLASH_RE, "");
const url = `${normalizedBase}/api/0/${path}`;
const headers = new Headers({ "Content-Type": "application/json" });
// URL-scoped: headers only attach when `url`'s origin matches the trusted
// host, so IAP tokens etc. can't leak to an attacker-controlled share URL.
applyCustomHeaders(headers, url);
warnIfSaasWithEnvCa(url);

let response: Response;
// The share URL's host may not be the logged-in one, so this goes out
// without the authenticated transport and its bearer token.
let response: Response | undefined;
const fetchUnauthenticated = async (
input: string | URL | Request,
init?: RequestInit,
) => {
response = await customFetch(input, init);
return response;
};

let result: Awaited<ReturnType<typeof getOrganizationSharedIssue>>;
try {
response = await customFetch(url, { headers });
result = await getOrganizationSharedIssue({
baseUrl: normalizedBase,
fetch: fetchUnauthenticated as unknown as typeof fetch,
headers,
parseAs: "json",
path: { organization_id_or_slug: orgSlug, share_id: shareId },
throwOnError: false,
});
} catch (error) {
if (error instanceof Error && isTlsCertError(error)) {
throw new ApiError(
"TLS certificate error",
0,
buildTlsErrorDetail(error),
path,
);
if (!(error instanceof SyntaxError)) {
throw error;
}
throw error;
throw new ApiError(
"Share link returned invalid JSON",
response?.status ?? 0,
undefined,
path,
);
}

if (!response.ok) {
if (response.status === 404) {
if ("error" in result) {
const status = result.response?.status;
if (status === undefined) {
const { error } = result;
if (error instanceof Error && isTlsCertError(error)) {
throw new ApiError(
"TLS certificate error",
0,
buildTlsErrorDetail(error),
path,
);
}
throw error;
}
if (status === 404) {
throw new ApiError(
"Share link not found or expired",
404,
Expand All @@ -759,29 +792,10 @@ export async function getSharedIssue(
path,
);
}
throw new ApiError(
"Failed to resolve share link",
response.status,
undefined,
path,
);
}

let data: unknown;
try {
data = await response.json();
} catch (error) {
if (!(error instanceof SyntaxError)) {
throw error;
}
throw new ApiError(
"Share link returned invalid JSON",
response.status,
undefined,
path,
);
throw new ApiError("Failed to resolve share link", status, undefined, path);
}

const data: unknown = result.data;
if (
typeof data !== "object" ||
data === null ||
Expand All @@ -791,7 +805,7 @@ export async function getSharedIssue(
) {
throw new ApiError(
"Share link response missing a valid issue ID",
response.status,
result.response?.status ?? 0,
undefined,
path,
);
Expand Down
50 changes: 33 additions & 17 deletions packages/cli/src/lib/api/org-auth-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
* login (`sentry auth login`) but reject personal and org auth tokens.
*/

import {
createOrganizationAuthToken,
deleteOrganizationAuthToken,
listOrganizationAuthTokens,
} from "@sentry/api";
import { array } from "valibot";

import { type OrgAuthToken, OrgAuthTokenSchema } from "../../types/index.js";
import { getControlSiloUrl } from "../sentry-client.js";
import {
apiRequestToRegion,
apiRequestToRegionNoContent,
} from "./infrastructure.js";
import { getSdkClientConfig } from "../sentry-sdk-client.js";
import { unwrapResult, unwrapValidatedResult } from "./infrastructure.js";

function tokensPath(orgSlug: string): string {
return `/organizations/${encodeURIComponent(orgSlug)}/org-auth-tokens/`;
Expand All @@ -34,12 +37,16 @@ function tokensPath(orgSlug: string): string {
export async function listOrgAuthTokens(
orgSlug: string,
): Promise<OrgAuthToken[]> {
const { data } = await apiRequestToRegion(
getControlSiloUrl(),
const result = await listOrganizationAuthTokens({
...getSdkClientConfig(getControlSiloUrl()),
path: { organization_id_or_slug: orgSlug },
});
return unwrapValidatedResult(
result,
"Failed to list org auth tokens",
array(OrgAuthTokenSchema),
tokensPath(orgSlug),
{ schema: array(OrgAuthTokenSchema) },
);
return data;
}

/**
Expand All @@ -56,12 +63,17 @@ export async function createOrgAuthToken(
orgSlug: string,
name: string,
): Promise<OrgAuthToken> {
const { data } = await apiRequestToRegion(
getControlSiloUrl(),
const result = await createOrganizationAuthToken({
...getSdkClientConfig(getControlSiloUrl()),
path: { organization_id_or_slug: orgSlug },
body: { name },
});
return unwrapValidatedResult(
result,
"Failed to create org auth token",
OrgAuthTokenSchema,
tokensPath(orgSlug),
{ method: "POST", body: { name }, schema: OrgAuthTokenSchema },
);
return data;
}

/**
Expand All @@ -74,9 +86,13 @@ export async function deleteOrgAuthToken(
orgSlug: string,
tokenId: string,
): Promise<void> {
await apiRequestToRegionNoContent(
getControlSiloUrl(),
`${tokensPath(orgSlug)}${encodeURIComponent(tokenId)}/`,
{ method: "DELETE" },
);
const result = await deleteOrganizationAuthToken({
...getSdkClientConfig(getControlSiloUrl()),
path: {
organization_id_or_slug: orgSlug,
// Sentry returns IDs as numeric strings; the path carries them as-is.
token_id: tokenId as unknown as number,
},
});
unwrapResult(result, "Failed to revoke org auth token");
}
22 changes: 11 additions & 11 deletions packages/cli/src/lib/api/releases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createOrganizationRelease,
createOrganizationReleaseDeploy,
deleteOrganizationRelease,
getOrganizationPreviousReleaseWithCommits,
getOrganizationRelease,
listOrganizationReleaseDeploys,
listOrganizationReleases,
Expand Down Expand Up @@ -37,14 +38,12 @@ import {
isSameRepository,
} from "../git.js";
import { logger } from "../logger.js";
import { resolveOrgRegion } from "../region.js";
import {
buildRepositorySettingsUrl,
buildSourceCodeIntegrationsUrl,
} from "../sentry-web-urls.js";
import {
API_MAX_PER_PAGE,
apiRequestToRegion,
getOrgSdkConfig,
MAX_PAGINATION_PAGES,
type PaginatedResponse,
Expand Down Expand Up @@ -421,16 +420,17 @@ async function getPreviousReleaseCommit(
): Promise<string | undefined> {
// oxlint-disable-next-line sentry-cli/no-silent-catch -- grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing.
try {
const regionUrl = await resolveOrgRegion(orgSlug);
const encodedVersion = encodeURIComponent(version);
const { data } = await apiRequestToRegion<{
lastCommit?: { id: string } | null;
}>(
regionUrl,
`organizations/${orgSlug}/releases/${encodedVersion}/previous-with-commits/`,
{ method: "GET" },
const result = await getOrganizationPreviousReleaseWithCommits({
...getOrgSdkConfig(),
path: { organization_id_or_slug: orgSlug, version },
});
// Sentry answers `{}` when no earlier release has commits.
const data = unwrapResult<{ lastCommit?: { id?: unknown } | null }>(
result,
"Failed to get previous release commit",
);
return data?.lastCommit?.id;
const id = data.lastCommit?.id;
return typeof id === "string" ? id : undefined;
} catch {
// Not critical — if we can't get the previous commit, we still send
// refs without previousCommit. Sentry will try to determine the range
Expand Down
Loading
Loading