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 @@ -190,18 +190,19 @@ Edit an issue alert rule
|-------|------|-------------|
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |
| `organizationId` | string | ID of the organization the alert belongs to |
| `createdBy` | string \| null | ID of the user who created the alert |
| `dateCreated` | string | When the alert was created (ISO 8601) |
| `dateUpdated` | string | When the alert was last updated (ISO 8601) |
| `triggers` | object \| null | Conditions on which the alert triggers |
| `type` | string | Type of the workflow, such as alert |
| `organizationId` | string | ID of the organization the workflow belongs to |
| `createdBy` | string \| null | ID of the user who created the workflow |
| `dateCreated` | string | When the workflow was created (ISO 8601) |
| `dateUpdated` | string | When the workflow was last updated (ISO 8601) |
| `triggers` | object \| null | Conditions on which the workflow triggers |
| `actionFilters` | array \| null | Filters to run before actions fire, each with the actions to fire |
| `environment` | string \| null | Name of the environment the alert is limited to. Null for all environments |
| `config` | object | Configuration of the alert, such as frequency in minutes |
| `detectorIds` | array \| null | IDs of the monitors connected to the alert |
| `enabled` | boolean | Whether the alert is enabled |
| `lastTriggered` | string \| null | When the alert last fired (ISO 8601) |
| `owner` | string \| null | Team or user that owns the alert, such as user:123 or team:456 |
| `environment` | string \| null | Name of the environment the workflow is limited to. Null for all environments |
| `config` | object | Configuration of the workflow, such as frequency in minutes |
| `detectorIds` | array \| null | IDs of the monitors connected to the workflow |
| `enabled` | boolean | Whether the workflow is enabled |
| `lastTriggered` | string \| null | When the workflow last fired (ISO 8601) |
| `owner` | string \| null | Team or user that owns the workflow, such as user:123 or team:456 |
| `org` | string | Organization slug |
| `project` | string | Project slug |
| `status` | string | Rule status (active, disabled) |
Expand Down Expand Up @@ -402,7 +403,7 @@ Edit a metric alert rule
| `dateUpdated` | string | When the monitor was last updated (ISO 8601) |
| `dataSources` | array \| null | Data sources the monitor evaluates, such as a query subscription |
| `conditionGroup` | object \| null | Conditions that decide the monitor's status |
| `config` | object | |
| `config` | object | Configuration of the monitor, which depends on its type |
| `enabled` | boolean | Whether the monitor is enabled |
| `org` | string | Organization slug |
| `status` | string | Rule status (active, disabled) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ View details of one or more events
| `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 |
| `occurrence` | object \| unknown \| null | |
| `occurrence` | object \| unknown \| null | Issue occurrence data for events in non-error issues, such as performance issues. Null otherwise |
| `_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 |
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export type AuthStatusData = {
* organization it can access use it even when stored OAuth is in effect.
*/
host?: string;
/** The token's user, from `/auth/` (not for organization auth tokens) */
/** The token's user, from `/users/me/` (not for organization auth tokens) */
user?: { name?: string; email?: string; username?: string };
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/auth/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* sentry auth whoami
*
* Display the currently authenticated identity. For user-scoped tokens (OAuth,
* PAT), this fetches the user from `/auth/`. For org auth tokens (`sntrys_`),
* PAT), this fetches the user from `/users/me/`. For org auth tokens (`sntrys_`),
* it extracts the organization from the token's embedded claim.
*/

Expand Down
4 changes: 0 additions & 4 deletions packages/cli/src/lib/api/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,10 +528,6 @@ export async function listTraceLogs(
* @param projectSlug - Project slug
* @param logId - The sentry.item_id of the log entry
* @param traceId - The trace ID (required by the endpoint)
*
* Uses the experimental /projects/{org}/{project}/trace-items/ endpoint directly via
* apiRequestToRegion — it is not yet available in @sentry/api (generated from
* getsentry/sentry-api-schema) because the endpoint is marked EXPERIMENTAL in Sentry.
*/
export function getLogItemDetail(
orgSlug: string,
Expand Down
22 changes: 12 additions & 10 deletions packages/cli/src/lib/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
listOrganizationProjectKeys,
listOrganizationProjects,
listProjectKeys,
resolveOrganizationDsn,
type ProjectKey as SdkProjectKey,
deleteProject as sdkDeleteProject,
getProject as sdkGetProject,
Expand All @@ -32,7 +33,6 @@ import { getCachedOrganizations } from "../db/regions.js";
import { reportCliError } from "../error-reporting.js";
import { ApiError, type AuthGuardSuccess, withAuthGuard } from "../errors.js";
import { logger } from "../logger.js";
import { resolveOrgRegion } from "../region.js";
import { getApiBaseUrl } from "../sentry-client.js";
import { isSentrySaasUrl } from "../sentry-urls.js";
import { buildProjectUrl } from "../sentry-web-urls.js";
Expand All @@ -49,6 +49,7 @@ import {
paginate,
unwrapPaginatedResult,
unwrapResult,
unwrapValidatedResult,
} from "./infrastructure.js";
import { listOrganizations } from "./organizations.js";

Expand Down Expand Up @@ -567,21 +568,22 @@ type DsnLookupOutcome =
| { kind: "miss" }
| { kind: "unsupported" };

/**
* Look a DSN up in one organization with `GET /organizations/{org}/dsn-lookup/`.
*
* The endpoint isn't in `@sentry/api` yet (it's still private), so this is
* a helper call until the publish lands.
*/
/** Look a DSN up in one organization with `GET /organizations/{org}/dsn-lookup/`. */
async function lookUpDsnInOrg(
org: SentryOrganization,
dsn: string,
): Promise<DsnLookupOutcome> {
try {
const { data } = await apiRequestToRegion(
await resolveOrgRegion(org.slug),
const result = await resolveOrganizationDsn({
...getOrgSdkConfig(),
path: { organization_id_or_slug: org.slug },
query: { dsn },
});
const data = unwrapValidatedResult(
result,
"Failed to look up DSN",
DsnLookupResponseSchema,
`/organizations/${encodeURIComponent(org.slug)}/dsn-lookup/`,
{ params: { dsn }, schema: DsnLookupResponseSchema },
);
return {
kind: "found",
Expand Down
31 changes: 17 additions & 14 deletions packages/cli/src/lib/api/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type ListOrganizationTraceItemAttributesResponse,
listOrganizationEvents,
listOrganizationTraceItemAttributes,
retrieveProjectTraceItem,
} from "@sentry/api";
import pLimit from "p-limit";

Expand Down Expand Up @@ -40,7 +41,6 @@ import { isAllDigits } from "../utils.js";

import {
API_MAX_PER_PAGE,
apiRequestToRegion,
getOrgSdkConfig,
type PaginatedResponse,
paginate,
Expand Down Expand Up @@ -204,11 +204,9 @@ type GetTraceItemDetailOptions = {
};

/**
* Fetch full attribute details for a single trace item via the experimental
* /projects/{org}/{project}/trace-items/{itemId}/ endpoint.
*
* This endpoint is not yet in @sentry/api (getsentry/sentry-api-schema) because
* it is marked EXPERIMENTAL in Sentry. Both span and log detail views use it.
* Fetch full attribute details for a single trace item via
* `GET /projects/{org}/{project}/trace-items/{itemId}/`, which Sentry
* publishes as experimental. Both span and log detail views use it.
*
* @param orgSlug - Organization slug
* @param projectSlug - Project slug
Expand All @@ -221,16 +219,21 @@ export async function getTraceItemDetail(
itemId: string,
{ traceId, itemType }: GetTraceItemDetailOptions,
): Promise<TraceItemDetail> {
const regionUrl = await resolveOrgRegion(orgSlug);
const { data } = await apiRequestToRegion<TraceItemDetail>(
regionUrl,
`/projects/${orgSlug}/${projectSlug}/trace-items/${itemId}/`,
{
params: { trace_id: traceId, item_type: itemType },
schema: TraceItemDetailSchema,
const result = await retrieveProjectTraceItem({
...getOrgSdkConfig(),
path: {
organization_id_or_slug: orgSlug,
project_id_or_slug: projectSlug,
item_id: itemId,
},
query: { trace_id: traceId, item_type: itemType },
});
return unwrapValidatedResult(
result,
"Failed to get trace item details",
TraceItemDetailSchema,
`/projects/${orgSlug}/${projectSlug}/trace-items/${itemId}/`,
);
return data;
}

/**
Expand Down
40 changes: 30 additions & 10 deletions packages/cli/src/lib/api/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,44 @@
* Functions for retrieving authenticated user information.
*/

import { retrieveUserDetails } from "@sentry/api";

import { type SentryUser, SentryUserSchema } from "../../types/index.js";

import { ApiError } from "../errors.js";
import { getControlSiloUrl } from "../sentry-client.js";
import { getSdkClientConfig } from "../sentry-sdk-client.js";

import { apiRequestToRegion } from "./infrastructure.js";
import { apiRequestToRegion, unwrapValidatedResult } from "./infrastructure.js";

/**
* Get the currently authenticated user's information.
*
* Uses the `/auth/` endpoint on the control silo, which works with all token
* types (OAuth, API tokens, OAuth App tokens). Unlike `/users/me/`, this
* endpoint does not return 403 for OAuth tokens.
* Reads `GET /users/me/` on the control silo. Servers older than
* getsentry/sentry#130368 return 403 there for every bearer token, so a 403
* falls back to the private `/auth/` endpoint, which accepts them.
*/
export async function getCurrentUser(): Promise<SentryUser> {
const { data } = await apiRequestToRegion<SentryUser>(
getControlSiloUrl(),
"/auth/",
{ schema: SentryUserSchema },
);
return data;
try {
const result = await retrieveUserDetails({
...getSdkClientConfig(getControlSiloUrl()),
path: { user_id: "me" },
});
return unwrapValidatedResult(
result,
"Failed to get current user",
SentryUserSchema,
"/users/me/",
);
} catch (error) {
if (!(error instanceof ApiError && error.status === 403)) {
throw error;
}
const { data } = await apiRequestToRegion<SentryUser>(
getControlSiloUrl(),
"/auth/",
{ schema: SentryUserSchema },
);
return data;
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/lib/db/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const ENV_ACCOUNT_ID_PREFIX = "env:";

/**
* Secret-free summary of an env token's virtual account on `host`, with the
* token's user when known (from `/auth/`). Never active: `sentry auth
* token's user when known (from `/users/me/`). Never active: `sentry auth
* switch` only selects stored accounts.
*/
export function toEnvAccountSummary(
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/env-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* `SENTRY_AUTH_TOKEN`/`SENTRY_TOKEN` with a trusted server acts as one
* more account next to stored logins (see `getEnvAccount` in `db/auth.ts`).
* It is never stored, so nothing records whose token it is; commands that
* list accounts ask the server (`/auth/`) when they show it.
* list accounts ask the server (`/users/me/`) when they show it.
*/

import { getCurrentUser } from "./api-client.js";
Expand All @@ -17,7 +17,7 @@ import type { SentryUser } from "../types/index.js";
const log = logger.withTag("auth.env-account");

/**
* The user behind the env token, from `/auth/` on its own server.
* The user behind the env token, from `/users/me/` on its own server.
* `undefined` for organization auth tokens (`sntrys_`), which belong to no
* user, and when the request fails: identifying the token is best-effort.
*/
Expand Down
10 changes: 3 additions & 7 deletions packages/cli/src/types/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,10 +294,7 @@ export type ProjectKey = Partial<SdkProjectKey> & {

// Internal types with Valibot schemas (runtime-validated, not in @sentry/api)

/**
* Response from `/api/0/organizations/{org}/dsn-lookup/`. The endpoint is
* private, so `@sentry/api` has no type for it.
*/
/** The fields the CLI reads from `/api/0/organizations/{org}/dsn-lookup/`. */
export const DsnLookupResponseSchema = looseObject({
organizationSlug: string(),
projectSlug: string(),
Expand All @@ -308,12 +305,11 @@ export const DsnLookupResponseSchema = looseObject({
// User

/**
* Minimal user schema for the `/auth/` endpoint response.
* The user fields the CLI reads from `/users/me/` and its `/auth/` fallback,
* which return the same body.
*
* All optional fields use `.nullish()` (accepts both `null` and `undefined`)
* because the Sentry API can return `null` for any of these.
* Note: `@sentry/api` doesn't export types for the `/auth/` endpoint —
* it's undocumented, so we define this schema manually.
*/
export const SentryUserSchema = looseObject({
id: string(),
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/commands/auth/switch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ describe("auth list", () => {

describe("with an env token", () => {
let originalFetch: typeof globalThis.fetch;
/** `<origin> <token>` of every `/auth/` request. */
/** `<origin> <token>` of every `/users/me/` request. */
let requests: string[];

beforeEach(async () => {
Expand All @@ -150,7 +150,7 @@ describe("auth list", () => {
const url = new URL(req.url);
const token = req.headers.get("Authorization")?.replace(/^Bearer /, "");
requests.push(`${url.origin} ${token}`);
if (url.pathname === "/api/0/auth/" && token === "env-token") {
if (url.pathname === "/api/0/users/me/" && token === "env-token") {
return Response.json({
id: "7",
email: "ci@example.com",
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/commands/org/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type Org = { id: string; slug: string; name: string; regionUrl: string };

/** Orgs each token can see; a token missing here gets a 401. */
let orgsByToken: Record<string, Org[]>;
/** The `/auth/` user of each token; a token missing here gets a 401. */
/** The `/users/me/` user of each token; a token missing here gets a 401. */
let usersByToken: Record<string, { id: string; email: string; name: string }>;
/** `<origin> <token>` for every org-list request, to check token routing. */
let requests: string[];
Expand All @@ -78,8 +78,8 @@ beforeEach(async () => {
const req = new Request(input, init);
const url = new URL(req.url);
const token = req.headers.get("Authorization")?.replace(/^Bearer /, "");
if (url.pathname === "/api/0/auth/") {
requests.push(`${url.origin} ${token} /auth/`);
if (url.pathname === "/api/0/users/me/") {
requests.push(`${url.origin} ${token} /users/me/`);
const user = token ? usersByToken[token] : undefined;
return user
? Response.json(user)
Expand Down Expand Up @@ -554,7 +554,7 @@ describe("org list with an env token", () => {
// Never sent to a stored account's server, nor stored tokens to SaaS
// on its behalf.
expect(requests.filter((r) => r.includes("env-token")).sort()).toEqual(
[`${SAAS} env-token`, `${SAAS} env-token /auth/`].sort(),
[`${SAAS} env-token`, `${SAAS} env-token /users/me/`].sort(),
);
expect(requests).toContain(`${ACME} acme-token`);
});
Expand Down
Loading
Loading