@sentry/api 1.25.0 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api-client/api-path.d.ts +20 -0
- package/dist/api-client/index.js +41 -1
- package/dist/api-client/transport.d.ts +17 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -553,7 +553,7 @@ Generic helpers delegate cancellation to your fetcher. Pass its signal through t
|
|
|
553
553
|
|
|
554
554
|
`@sentry/api/client` is the request layer the Sentry CLI and MCP servers share, built on the generated operations. It uses only web-standard APIs (`fetch`, `Request`, `Headers`, `URL`, `AbortSignal`, `crypto.subtle`), so it runs in Node, Cloudflare Workers, and browsers. Hosts inject what differs between them:
|
|
555
555
|
|
|
556
|
-
- **Transport.** `createApiTransport` sets `Authorization` from a `CredentialProvider`, refreshes the credential once after a 401, times out each attempt, retries through the shared retry policy (429 for any method; 500, 502, 503, 504, timeouts, and network errors only for GET, HEAD, OPTIONS, or writes marked idempotent), follows redirects only to trusted origins, and serves and invalidates cached GETs. The host passes credentials, a `TransportCache`, the request-scoped `fresh` hook, a logger, a span hook, and its error classes.
|
|
556
|
+
- **Transport.** `createApiTransport` sets `Authorization` from a `CredentialProvider`, refreshes the credential once after a 401, times out each attempt, retries through the shared retry policy (429 for any method; 500, 502, 503, 504, timeouts, and network errors only for GET, HEAD, OPTIONS, or writes marked idempotent), follows redirects only to trusted origins, and serves and invalidates cached GETs. `CredentialProvider.current` receives the request URL and the organization and project its path targets (`parseApiPathScope`), so a host with several logins can pick the one that can reach that organization. The host passes credentials, a `TransportCache`, the request-scoped `fresh` hook, a logger, a span hook, and its error classes.
|
|
557
557
|
- **Request core.** `createApiClientCore` sends `<origin>/api/0<path>` through the transport and throws the status's `ApiError` subclass for an error response. `sdkClient` gives a generated operation a client that sends through the same path.
|
|
558
558
|
- **Response cache.** The cache policy (`lookupResponse`, `storeResponse`, tiers, identity-scoped keys, invalidation) works on any `CacheStore` and `BodyCodec`. `createMemoryStore` is a byte-budgeted in-memory store. Compressing codecs need host APIs such as `node:zlib`, so hosts supply them.
|
|
559
559
|
- **Validation.** Endpoint wrappers validate responses with any [Standard Schema](https://standardschema.dev) validator the host passes in, such as the `@sentry/api/zod` or `@sentry/api/valibot` schemas. The client depends on neither.
|
|
@@ -30,5 +30,25 @@ export declare function buildSentryApiUrl(baseUrl: string, endpoint: string): st
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
export declare function apiPath(strings: TemplateStringsArray, ...values: (string | number)[]): string;
|
|
33
|
+
/** The organization, and project when present, an API path targets. */
|
|
34
|
+
export type ApiPathScope = {
|
|
35
|
+
organization: string;
|
|
36
|
+
project?: string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The organization, and project when present, a Sentry API request targets,
|
|
40
|
+
* read from its path: `/organizations/{org}/`, `/projects/{org}/{project}/`,
|
|
41
|
+
* `/teams/{org}/`, and `/organizations/{org}/projects/{project}/`.
|
|
42
|
+
*
|
|
43
|
+
* Accepts a full URL, whose path must contain `/api/0/` (a self-hosted
|
|
44
|
+
* install may serve it under a prefix), or a path relative to `/api/0/` with
|
|
45
|
+
* or without that prefix. Undefined for unscoped paths, list endpoints such
|
|
46
|
+
* as `/organizations/`, and schema placeholders.
|
|
47
|
+
*
|
|
48
|
+
* Hosts with several credentials use it to pick the one that can reach the
|
|
49
|
+
* organization; {@link createApiTransport} passes it to
|
|
50
|
+
* `CredentialProvider.current`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseApiPathScope(urlOrPath: string): ApiPathScope | undefined;
|
|
33
53
|
/** `path` with `query` appended, or `path` alone when `query` is empty. */
|
|
34
54
|
export declare function withQuery(path: string, query: URLSearchParams): string;
|
package/dist/api-client/index.js
CHANGED
|
@@ -240,6 +240,41 @@ function apiPath(strings, ...values) {
|
|
|
240
240
|
return acc + literal + encoded;
|
|
241
241
|
}, "");
|
|
242
242
|
}
|
|
243
|
+
var API_PATH_IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
244
|
+
function isApiPathIdentifier(segment) {
|
|
245
|
+
return segment !== void 0 && API_PATH_IDENTIFIER.test(segment);
|
|
246
|
+
}
|
|
247
|
+
function apiPathSegments(urlOrPath) {
|
|
248
|
+
const isUrl = URL.canParse(urlOrPath);
|
|
249
|
+
const path = isUrl ? new URL(urlOrPath).pathname : urlOrPath.split(/[?#]/, 1)[0] ?? "";
|
|
250
|
+
const segments = path.split("/").filter((segment) => segment.length > 0);
|
|
251
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
252
|
+
if (segments[index] === "api" && segments[index + 1] === "0") {
|
|
253
|
+
return segments.slice(index + 2);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return isUrl ? void 0 : segments;
|
|
257
|
+
}
|
|
258
|
+
function parseApiPathScope(urlOrPath) {
|
|
259
|
+
const segments = apiPathSegments(urlOrPath);
|
|
260
|
+
if (!segments) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const [resource, organization, next, project] = segments;
|
|
264
|
+
if (!isApiPathIdentifier(organization)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (resource === "organizations") {
|
|
268
|
+
return next === "projects" && isApiPathIdentifier(project) ? { organization, project } : { organization };
|
|
269
|
+
}
|
|
270
|
+
if (resource === "projects") {
|
|
271
|
+
return isApiPathIdentifier(next) ? { organization, project: next } : { organization };
|
|
272
|
+
}
|
|
273
|
+
if (resource === "teams") {
|
|
274
|
+
return { organization };
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
243
278
|
function withQuery(path, query) {
|
|
244
279
|
const queryString = query.toString();
|
|
245
280
|
return queryString ? `${path}?${queryString}` : path;
|
|
@@ -1703,7 +1738,7 @@ function createApiTransport(config) {
|
|
|
1703
1738
|
const run = async () => {
|
|
1704
1739
|
const startTime = performance.now();
|
|
1705
1740
|
const elapsed = () => (performance.now() - startTime).toFixed(0);
|
|
1706
|
-
const credential = credentials ? await credentials.current() : ANONYMOUS;
|
|
1741
|
+
const credential = credentials ? await credentials.current(credentialRequest(url)) : ANONYMOUS;
|
|
1707
1742
|
const lookup = cache && method === "GET" && !options.validateRedirects && !config.fresh?.() ? await cache.lookup({
|
|
1708
1743
|
url,
|
|
1709
1744
|
requestHeaders: lookupHeaders(credential),
|
|
@@ -1737,6 +1772,10 @@ function createApiTransport(config) {
|
|
|
1737
1772
|
}
|
|
1738
1773
|
return { request };
|
|
1739
1774
|
}
|
|
1775
|
+
function credentialRequest(url) {
|
|
1776
|
+
const scope = parseApiPathScope(url);
|
|
1777
|
+
return scope ? { url, scope } : { url };
|
|
1778
|
+
}
|
|
1740
1779
|
async function drainBody(response, signal) {
|
|
1741
1780
|
const drained = response.body?.pipeTo(new WritableStream());
|
|
1742
1781
|
if (!drained) {
|
|
@@ -1858,6 +1897,7 @@ export {
|
|
|
1858
1897
|
normalizeStatsPeriod,
|
|
1859
1898
|
normalizeUrl,
|
|
1860
1899
|
paginate,
|
|
1900
|
+
parseApiPathScope,
|
|
1861
1901
|
parseRetryDelayMs,
|
|
1862
1902
|
parseStatsPeriod,
|
|
1863
1903
|
readErrorResponse,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* and spans.
|
|
11
11
|
*/
|
|
12
12
|
import { type RetryPolicy } from "./retry.js";
|
|
13
|
+
import { type ApiPathScope } from "./api-path.js";
|
|
13
14
|
import type { CacheLookup } from "./response-cache/policy.js";
|
|
14
15
|
type FetchInput = Request | string | URL;
|
|
15
16
|
/** The fields the transport reads; hosts extend it with their own. */
|
|
@@ -18,9 +19,23 @@ export type TransportCredential = {
|
|
|
18
19
|
/** Stable hash of the login, for cache scoping; never a raw token. */
|
|
19
20
|
identity: string;
|
|
20
21
|
};
|
|
22
|
+
/** The request a credential is chosen for; see {@link CredentialProvider.current}. */
|
|
23
|
+
export type CredentialRequest = {
|
|
24
|
+
url: string;
|
|
25
|
+
/**
|
|
26
|
+
* The organization, and project when present, the URL path targets
|
|
27
|
+
* ({@link parseApiPathScope}). Absent for unscoped paths.
|
|
28
|
+
*/
|
|
29
|
+
scope?: ApiPathScope;
|
|
30
|
+
};
|
|
21
31
|
export type CredentialProvider<C extends TransportCredential> = {
|
|
22
|
-
/**
|
|
23
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The credential for a new request, before any refresh. Cache lookups use
|
|
34
|
+
* it. A host with several logins can use `request.scope` to pick the one
|
|
35
|
+
* that can reach the organization. The URL is already built, so the
|
|
36
|
+
* credential must be valid for its origin.
|
|
37
|
+
*/
|
|
38
|
+
current(request: CredentialRequest): C | Promise<C>;
|
|
24
39
|
/** Called after a cache miss, before the first attempt, to refresh ahead of expiry. */
|
|
25
40
|
prepare?(credential: C): Promise<C>;
|
|
26
41
|
/** After a 401: the refreshed credential, or undefined when it cannot refresh. */
|