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
8 changes: 4 additions & 4 deletions docs/contributing/api-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,15 @@ exit-coded `ApiError`. See
### Automatic Retries

`SentryApiService` sends every request, including `@sentry/api` operations,
through the shared transport in `@sentry/toolkit-core/api-transport`, which
through the shared transport in `@sentry/toolkit-core/api-client/transport`, which
retries failed requests using the shared policy in
`@sentry/toolkit-core/api-retry`. `SentryApiService` never retries on top, so
`@sentry/toolkit-core/api-client/retry`. `SentryApiService` never retries on top, so
a request is retried by one layer only. This page describes the
behavior; the values live only in code, so read them there instead of copying
them here:

- Which statuses retry, and for which methods: `RETRYABLE_STATUSES` in
[`api-retry.ts`](../../packages/toolkit-core/src/api-retry.ts). In short, a
[`retry.ts`](../../packages/toolkit-core/src/api-client/retry.ts). In short, a
rate-limited request retries for any method, a transient server error only
for an idempotent one, and other errors never.
- How many retries and how long they may wait: `REQUEST_RETRY_POLICY` in
Expand Down Expand Up @@ -305,7 +305,7 @@ Callers and tools need no changes: a retried request either resolves normally or
throws the same error it would have thrown without retries.

The CLI sends its requests through the shared transport in
`@sentry/toolkit-core/api-transport`, configured in
`@sentry/toolkit-core/api-client/transport`, configured in
`packages/cli/src/lib/sentry-client.ts`. It follows the same shared rule; only
the caps differ (`RETRY_POLICY`): the hosted MCP keeps its total wait short so
a Workers request isn't held open, while the CLI can wait longer. The CLI also
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/script/generate-api-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { mkdir, writeFile } from "node:fs/promises";
import { dirname } from "node:path";
import { operations } from "@sentry/api/operations";
import { isIdempotentRequest } from "@sentry/toolkit-core/api-retry";
import { isIdempotentRequest } from "@sentry/toolkit-core/api-client/retry";
import type { IdempotentOperation } from "../src/lib/idempotent-operations.js";
import { projectApiOperations } from "./lib/api-schema.js";

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/lib/api/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
fetchAllPages as fetchEveryPage,
type FetchAllPagesFailure,
paginate as paginateFrom,
} from "@sentry/toolkit-core/pagination";
} from "@sentry/toolkit-core/api-client/pagination";
// oxlint-disable-next-line sentry-cli/no-namespace-import -- Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node";
import { type GenericSchema, safeParse } from "valibot";
Expand Down Expand Up @@ -493,7 +493,7 @@ export async function autoPaginate<T>(
* This is the only supported way to build a limit-bounded list; the
* `sentry-cli/no-manual-pagination` lint rule rejects hand-written loops
* (#1458, #1608). Page sizing and resume-safe cursors live in
* `@sentry/toolkit-core/pagination`, shared with the MCP; this wrapper sets
* `@sentry/toolkit-core/api-client/pagination`, shared with the MCP; this wrapper sets
* the CLI's page cap and default limit and logs the CLI's warnings.
*
* Callers own region resolution and query building inside `fetchPage`, and
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/fetch-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
* implementation; this path stays for CLI callers and tests.
*/

export { extractInputUrl } from "@sentry/toolkit-core/api-transport";
export { extractInputUrl } from "@sentry/toolkit-core/api-client/transport";
2 changes: 1 addition & 1 deletion packages/cli/src/lib/response-cache-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
type CacheStore,
classifyUrl,
FALLBACK_TTL_MS,
} from "@sentry/toolkit-core/response-cache";
} from "@sentry/toolkit-core/api-client/response-cache";
import type { BodyEncoding } from "@sentry/toolkit-core/response-cache/codec";
import pLimit from "p-limit";

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lib/response-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
classifyUrl,
lookupResponse,
storeResponse,
} from "@sentry/toolkit-core/response-cache";
} from "@sentry/toolkit-core/api-client/response-cache";
import { createBodyCodec } from "@sentry/toolkit-core/response-cache/codec";

import { getIdentityFingerprint } from "./db/auth.js";
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/lib/sentry-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@

import type { SentryClient } from "@sentry/api";
import { getTraceData } from "@sentry/node";
import type { RetryPolicy } from "@sentry/toolkit-core/api-retry";
import type { RetryPolicy } from "@sentry/toolkit-core/api-client/retry";
import {
createApiTransport,
extractUrlPath,
getValidatedResponseSource,
type ResponseBodyMode,
type TransportCache,
} from "@sentry/toolkit-core/api-transport";
import { computeInvalidationPrefixes } from "@sentry/toolkit-core/response-cache";
} from "@sentry/toolkit-core/api-client/transport";
import { computeInvalidationPrefixes } from "@sentry/toolkit-core/api-client/response-cache";
import { formatAuthHeader, normalizeAuthToken } from "./auth-header.js";
import { seedCompletions } from "./api/completion-seed.js";
import { maybeWarnEnvTokenIgnored } from "./auth-hint.js";
Expand Down Expand Up @@ -87,7 +87,7 @@ const ENDPOINT_TIMEOUT_OVERRIDES: TimeoutOverride[] = [

/**
* Caps for automatic retries. Which statuses and methods retry is the shared
* rule in `@sentry/toolkit-core/api-retry`.
* rule in `@sentry/toolkit-core/api-client/retry`.
*
* `maxDelayMs` caps the jittered backoff and the server-requested wait
* (`Retry-After`, rate-limit reset); a longer server-requested wait is not
Expand Down Expand Up @@ -331,7 +331,7 @@ async function withHttpSpan(
/**
* The CLI's transport options: stored credentials, host-scope checks, the
* response cache, and the CLI's error classes. Retries, timeouts, the 401
* refresh, and redirect validation are in `@sentry/toolkit-core/api-transport`.
* refresh, and redirect validation are in `@sentry/toolkit-core/api-client/transport`.
*/
function createCliTransport(credential: CredentialContext | undefined) {
return createApiTransport<CredentialContext>({
Expand Down Expand Up @@ -391,7 +391,7 @@ function createCliTransport(credential: CredentialContext | undefined) {
/**
* Create a fetch function with authentication, timeout, retry, caching, and 401 refresh.
*
* This wraps the native fetch with `@sentry/toolkit-core/api-transport`:
* This wraps the native fetch with `@sentry/toolkit-core/api-client/transport`:
* - **Response caching** for GET requests (checked before hitting the network)
* - Auth token injection (Bearer token)
* - Request timeout via AbortController
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/lib/response-cache-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { CacheEntry } from "@sentry/toolkit-core/response-cache";
import type { CacheEntry } from "@sentry/toolkit-core/api-client/response-cache";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import {
createFileStore,
Expand Down
6 changes: 3 additions & 3 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
} from "@sentry/api";
import { buildSentryApiUrl } from "@sentry/toolkit-core/api-request";
import { sentryBearerHeader } from "@sentry/toolkit-core/auth-token";
import type { RetryPolicy } from "@sentry/toolkit-core/api-retry";
import { createApiTransport } from "@sentry/toolkit-core/api-transport";
import type { RetryPolicy } from "@sentry/toolkit-core/api-client/retry";
import { createApiTransport } from "@sentry/toolkit-core/api-client/transport";
import { z } from "zod";
import { DEFAULT_SEARCH_ISSUES_PERIOD } from "../constants";
import { ConfigurationError } from "../errors";
Expand Down Expand Up @@ -316,7 +316,7 @@ type RequestOptions = {

/**
* Caps for automatic retries. Which statuses and methods are retried is the
* shared rule in `@sentry/toolkit-core/api-retry`.
* shared rule in `@sentry/toolkit-core/api-client/retry`.
*
* Sentry's per-endpoint limits are mostly per second ("Limit is 40 requests
* in 1 seconds"), so a 429's reset is usually under a second away and two
Expand Down
13 changes: 8 additions & 5 deletions packages/toolkit-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ builds bundle this private workspace package into their artifacts.
The shared code validates and formats upstream bearer tokens, assembles Sentry
API URLs, constructs OAuth device-flow form bodies, classifies RFC 8628 polling
responses, advances retry intervals, decides when a Sentry API request may
be retried and how long to wait (`api-retry`), recognizes Sentry hostnames and
be retried and how long to wait (`api-client/retry`), recognizes Sentry hostnames and
credential-safe SaaS HTTPS origins, encodes API path identifiers, parses Events
field expressions the way the Sentry frontend does, parses search queries with
the Sentry frontend grammar, resolves Events endpoint sorts so the sort column
Expand Down Expand Up @@ -37,13 +37,16 @@ the one exception.
rate-limit wait, and each host maps that to its own errors.
`readJsonResponse` parses a successful body after checking its
Content-Type.
- `pagination`, `retry`, `transport`, and `response-cache` are described in
the sections below. The `node:zlib` body codec stays outside, at
`response-cache/codec`, since browsers lack it.

## API transport

`api-transport` is the authenticated fetch both products send Sentry API
`api-client/transport` is the authenticated fetch both products send Sentry API
requests through. `createApiTransport` sets the Authorization header, refreshes
the credential once after a 401 and replays the request (any method, since the
endpoint never ran), times out each attempt, and retries through `api-retry`.
endpoint never ran), times out each attempt, and retries through `api-client/retry`.
Request bodies are replayed on each attempt, including stream bodies.
With `validateRedirects`, it follows redirects by hand, sends the credential
only to origins the host trusts, and records the final origin for
Expand All @@ -67,7 +70,7 @@ skips the cache, since entries are scoped to a credential. The MCP's

## Pagination

`pagination` walks Sentry list endpoints up to a row limit. `paginate` splits
`api-client/pagination` walks Sentry list endpoints up to a row limit. `paginate` splits
the limit evenly over the fewest requests (at most 100 rows each), reuses the
page size a page-number cursor encodes, and returns a cursor that resumes at
the first row the caller has not received, even when the walk stopped inside
Expand All @@ -84,7 +87,7 @@ CLI's `paginate` and `fetchAllPages` in

## Response cache

`response-cache` holds the cache policy for Sentry API GET responses.
`api-client/response-cache` holds the cache policy for Sentry API GET responses.
`classifyUrl` puts a URL in a fallback TTL tier for responses without HTTP
caching headers: `volatile` (60 seconds) for issues, project event details,
full traces, and log and span queries; `no-cache` for autofix and root-cause
Expand Down
20 changes: 4 additions & 16 deletions packages/toolkit-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
"types": "./src/api-request.ts",
"default": "./src/api-request.ts"
},
"./api-retry": {
"types": "./src/api-retry.ts",
"default": "./src/api-retry.ts"
},
"./api-transport": {
"types": "./src/api-transport.ts",
"default": "./src/api-transport.ts"
},
"./api-path-segment": {
"types": "./src/api-path-segment.ts",
"default": "./src/api-path-segment.ts"
Expand Down Expand Up @@ -47,10 +39,6 @@
"types": "./src/oauth-poll.ts",
"default": "./src/oauth-poll.ts"
},
"./pagination": {
"types": "./src/pagination.ts",
"default": "./src/pagination.ts"
},
"./region-cache": {
"types": "./src/region-cache.ts",
"default": "./src/region-cache.ts"
Expand All @@ -59,10 +47,6 @@
"types": "./src/resource-identity.ts",
"default": "./src/resource-identity.ts"
},
"./response-cache": {
"types": "./src/response-cache/index.ts",
"default": "./src/response-cache/index.ts"
},
"./response-cache/codec": {
"types": "./src/response-cache/codec.ts",
"default": "./src/response-cache/codec.ts"
Expand All @@ -79,6 +63,10 @@
"types": "./src/sentry-origin.ts",
"default": "./src/sentry-origin.ts"
},
"./api-client/response-cache": {
"types": "./src/api-client/response-cache/index.ts",
"default": "./src/api-client/response-cache/index.ts"
},
"./api-client/*": {
"types": "./src/api-client/*.ts",
"default": "./src/api-client/*.ts"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createBodyCodec, type EncodedBody } from "./codec.js";
import {
createBodyCodec,
type EncodedBody,
} from "../../response-cache/codec.js";
import { buildCacheKey } from "./keys.js";
import { createMemoryStore, ENTRY_OVERHEAD_BYTES } from "./memory-store.js";
import type { CacheEntry } from "./store.js";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createBodyCodec } from "./codec.js";
import { createBodyCodec } from "../../response-cache/codec.js";
import { createMemoryStore } from "./memory-store.js";
import {
type CacheLookup,
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit-core/src/api-client/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* errors: the MCP to `ApiError` subclasses, the CLI to its exit-coded ones.
*/

import { parseRetryDelayMs } from "../api-retry.js";
import { parseRetryDelayMs } from "./retry.js";

/** What a failed response's body says, read once. */
export type ErrorResponseBody = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
parseRetryDelayMs,
type RetryPolicy,
sleep,
} from "./api-retry.js";
} from "./retry.js";

const NOW = Date.UTC(2026, 0, 1, 12, 0, 0, 300);
const NOW_SECONDS = Math.floor(NOW / 1000);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RetryPolicy } from "./api-retry.js";
import type { RetryPolicy } from "./retry.js";
import {
ApiTimeoutError,
type ApiTransportOptions,
createApiTransport,
getValidatedResponseSource,
type TransportCache,
type TransportCredential,
} from "./api-transport.js";
} from "./transport.js";
import {
buildCacheKey,
createMemoryStore,
lookupResponse,
storeResponse,
} from "./response-cache/index.js";
import { createBodyCodec } from "./response-cache/codec.js";
import { createBodyCodec } from "../response-cache/codec.js";

const URL_BASE = "https://us.sentry.io/api/0";
const ISSUES_URL = `${URL_BASE}/organizations/acme/issues/`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*
* Per request it sets the Authorization header, refreshes the credential once
* after a 401 (any method), times out each attempt, retries through
* `api-retry`, replays request bodies, optionally follows redirects only to
* `retry`, replays request bodies, optionally follows redirects only to
* trusted origins, and serves and invalidates cached GETs through an injected
* cache. The host supplies credentials, header rules, error classes, logging,
* and spans.
Expand All @@ -16,8 +16,8 @@ import {
parseRetryDelayMs,
type RetryPolicy,
sleep,
} from "./api-retry.js";
import { sentryBearerHeader, trimAuthToken } from "./auth-token.js";
} from "./retry.js";
import { sentryBearerHeader, trimAuthToken } from "../auth-token.js";
import type { CacheLookup } from "./response-cache/policy.js";

type FetchInput = Request | string | URL;
Expand Down
11 changes: 9 additions & 2 deletions packages/toolkit-core/src/response-cache/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
// A namespace import, so a missing `zstdCompress` (Node < 22.15) is
// `undefined` rather than a link error.
import * as nodeZlib from "node:zlib";
import type { BodyCodec, EncodedBody } from "./body.js";
import type {
BodyCodec,
EncodedBody,
} from "../api-client/response-cache/body.js";

export type { BodyCodec, BodyEncoding, EncodedBody } from "./body.js";
export type {
BodyCodec,
BodyEncoding,
EncodedBody,
} from "../api-client/response-cache/body.js";

/** Bodies smaller than this are stored uncompressed. */
export const MIN_COMPRESSED_BYTES = 256;
Expand Down
Loading