@sentry/api 1.27.0 → 1.29.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 CHANGED
@@ -176,6 +176,29 @@ const sentry = createSentryClient(
176
176
  const { data, error } = await sentry.listOrganizations();
177
177
  ```
178
178
 
179
+ ### Routing requests by organization
180
+
181
+ Set `resolveBaseUrl` to pick each request's base URL from its path before the
182
+ URL is built, such as the region that serves the organization. It receives the
183
+ configured `baseUrl` (`''` when unset), the path and query that follow it, and
184
+ the organization and project the path targets (`parseApiPathScope` from
185
+ `@sentry/api/client`; absent for unscoped paths). Its result replaces `baseUrl`
186
+ for that request only; trailing slashes are removed.
187
+
188
+ ```ts
189
+ const sentry = createSentryClient({
190
+ baseUrl: "https://sentry.io",
191
+ resolveBaseUrl: async ({ baseUrl, scope }) =>
192
+ scope ? await regionUrlFor(scope.organization) : baseUrl,
193
+ });
194
+ // Sent to the organization's region, such as https://us.sentry.io.
195
+ await sentry.listOrganizationIssues({ path: { organization_id_or_slug: "acme" } });
196
+ ```
197
+
198
+ It runs once per request, including `prepareRequest`, after `requestValidator`
199
+ and before request interceptors. A per-call `resolveBaseUrl` replaces the
200
+ configured one. A throw rejects the request unchanged, before `fetch` is called.
201
+
179
202
  ### Preparing a raw request
180
203
 
181
204
  Use `sentry.client.prepareRequest` when a consumer needs to inspect a request
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Reading the organization and project an API path targets. No imports, so
3
+ * the generated client can use it without pulling in `@sentry/api/client`.
4
+ */
5
+ /** The organization, and project when present, an API path targets. */
6
+ export type ApiPathScope = {
7
+ organization: string;
8
+ project?: string;
9
+ };
10
+ /**
11
+ * The organization, and project when present, a Sentry API request targets,
12
+ * read from its path: `/organizations/{org}/`, `/projects/{org}/{project}/`,
13
+ * `/teams/{org}/`, and `/organizations/{org}/projects/{project}/`.
14
+ *
15
+ * Accepts a full URL, whose path must contain `/api/0/` (a self-hosted
16
+ * install may serve it under a prefix), or a path relative to `/api/0/` with
17
+ * or without that prefix. Undefined for unscoped paths, list endpoints such
18
+ * as `/organizations/`, and schema placeholders.
19
+ *
20
+ * Hosts with several credentials or regions use it to pick the one that
21
+ * serves the organization: `createApiTransport` passes it to
22
+ * `CredentialProvider.current`, and the generated client to
23
+ * `Config.resolveBaseUrl`.
24
+ */
25
+ export declare function parseApiPathScope(urlOrPath: string): ApiPathScope | undefined;
@@ -1,3 +1,4 @@
1
+ export { type ApiPathScope, parseApiPathScope } from "./api-path-scope.js";
1
2
  /**
2
3
  * Encode one Sentry API path identifier without letting it become a URL path.
3
4
  *
@@ -30,25 +31,5 @@ export declare function buildSentryApiUrl(baseUrl: string, endpoint: string): st
30
31
  * ```
31
32
  */
32
33
  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;
53
34
  /** `path` with `query` appended, or `path` alone when `query` is empty. */
54
35
  export declare function withQuery(path: string, query: URLSearchParams): string;
@@ -212,34 +212,7 @@ function isApiAuthenticationErrorDeep(error) {
212
212
  return false;
213
213
  }
214
214
 
215
- // src/api-client/api-path.ts
216
- function encodeApiPathSegment(value) {
217
- const text = String(value);
218
- return text === "." || text === ".." ? null : encodeURIComponent(text);
219
- }
220
- function buildSentryApiUrl(baseUrl, endpoint) {
221
- const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
222
- let end = baseUrl.length;
223
- while (end > 0 && baseUrl.charAt(end - 1) === "/") {
224
- end -= 1;
225
- }
226
- return `${baseUrl.slice(0, end)}/api/0/${path}`;
227
- }
228
- function apiPath(strings, ...values) {
229
- return strings.reduce((acc, literal, index) => {
230
- if (index >= values.length) {
231
- return acc + literal;
232
- }
233
- const value = String(values[index]);
234
- const encoded = encodeApiPathSegment(value);
235
- if (encoded === null) {
236
- throw new UserInputError(
237
- `Invalid identifier "${value}": relative path segments are not allowed.`
238
- );
239
- }
240
- return acc + literal + encoded;
241
- }, "");
242
- }
215
+ // src/api-client/api-path-scope.ts
243
216
  var API_PATH_IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
244
217
  function isApiPathIdentifier(segment) {
245
218
  return segment !== void 0 && API_PATH_IDENTIFIER.test(segment);
@@ -275,6 +248,35 @@ function parseApiPathScope(urlOrPath) {
275
248
  }
276
249
  return;
277
250
  }
251
+
252
+ // src/api-client/api-path.ts
253
+ function encodeApiPathSegment(value) {
254
+ const text = String(value);
255
+ return text === "." || text === ".." ? null : encodeURIComponent(text);
256
+ }
257
+ function buildSentryApiUrl(baseUrl, endpoint) {
258
+ const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
259
+ let end = baseUrl.length;
260
+ while (end > 0 && baseUrl.charAt(end - 1) === "/") {
261
+ end -= 1;
262
+ }
263
+ return `${baseUrl.slice(0, end)}/api/0/${path}`;
264
+ }
265
+ function apiPath(strings, ...values) {
266
+ return strings.reduce((acc, literal, index) => {
267
+ if (index >= values.length) {
268
+ return acc + literal;
269
+ }
270
+ const value = String(values[index]);
271
+ const encoded = encodeApiPathSegment(value);
272
+ if (encoded === null) {
273
+ throw new UserInputError(
274
+ `Invalid identifier "${value}": relative path segments are not allowed.`
275
+ );
276
+ }
277
+ return acc + literal + encoded;
278
+ }, "");
279
+ }
278
280
  function withQuery(path, query) {
279
281
  const queryString = query.toString();
280
282
  return queryString ? `${path}?${queryString}` : path;
@@ -6,5 +6,5 @@ export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen';
6
6
  export type { ServerSentEventsResult } from '../core/serverSentEvents.gen';
7
7
  export { createClient } from './client.gen';
8
8
  export type { PreparedClient } from './client.gen';
9
- export type { Client, ClientOptions, Config, CreateClientConfig, Options, RequestOptions, RequestResult, ResolvedRequestOptions, ResponseStyle, TDataShape, } from './types.gen';
9
+ export type { BaseUrlRequest, BaseUrlResolver, Client, ClientOptions, Config, CreateClientConfig, Options, RequestOptions, RequestResult, ResolvedRequestOptions, ResponseStyle, TDataShape, } from './types.gen';
10
10
  export { createConfig, mergeHeaders } from './utils.gen';
@@ -3,6 +3,21 @@ import type { Auth } from '../core/auth.gen';
3
3
  import type { ServerSentEventsOptions, ServerSentEventsResult } from '../core/serverSentEvents.gen';
4
4
  import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen';
5
5
  import type { Middleware } from './utils.gen';
6
+ import type { ApiPathScope } from '../api-client/api-path-scope';
7
+ /** The request a base URL is picked for; see {@link Config.resolveBaseUrl}. */
8
+ export type BaseUrlRequest = {
9
+ /** The configured base URL, or `''` when none is set. */
10
+ baseUrl: string;
11
+ /** The request's path and query, as they follow the base URL. */
12
+ url: string;
13
+ /**
14
+ * The organization, and project when present, the path targets
15
+ * (`parseApiPathScope` in `@sentry/api/client`). Absent for unscoped paths.
16
+ */
17
+ scope?: ApiPathScope;
18
+ };
19
+ /** Picks the base URL for one request; see {@link Config.resolveBaseUrl}. */
20
+ export type BaseUrlResolver = (request: BaseUrlRequest) => string | Promise<string>;
6
21
  export type ResponseStyle = 'fields';
7
22
  export interface Config<T extends ClientOptions = ClientOptions> extends Omit<RequestInit, 'body' | 'headers' | 'method'>, CoreConfig {
8
23
  /**
@@ -16,6 +31,13 @@ export interface Config<T extends ClientOptions = ClientOptions> extends Omit<Re
16
31
  * @default globalThis.fetch
17
32
  */
18
33
  fetch?: typeof fetch;
34
+ /**
35
+ * Pick the base URL for each request, before its URL is built, such as the
36
+ * region that serves the organization the path targets. Called once per
37
+ * request, after `requestValidator` and before request interceptors; its
38
+ * result replaces `baseUrl` for that request.
39
+ */
40
+ resolveBaseUrl?: BaseUrlResolver;
19
41
  /**
20
42
  * Please don't use the Fetch client for Next.js applications. The `next`
21
43
  * options won't have any effect.
package/dist/index.d.ts CHANGED
@@ -9,4 +9,4 @@ export type * from './query-enums.gen.ts';
9
9
  export * from './pagination.gen.ts';
10
10
  export { createSentryClient } from './sentry-client.gen.ts';
11
11
  export type { SentryClient } from './sentry-client.gen.ts';
12
- export type { Config } from './client/index.ts';
12
+ export type { BaseUrlRequest, BaseUrlResolver, Config } from './client/index.ts';
package/dist/index.js CHANGED
@@ -443,6 +443,43 @@ function getValidRequestBody(options) {
443
443
  return void 0;
444
444
  }
445
445
 
446
+ // src/api-client/api-path-scope.ts
447
+ var API_PATH_IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
448
+ function isApiPathIdentifier(segment) {
449
+ return segment !== void 0 && API_PATH_IDENTIFIER.test(segment);
450
+ }
451
+ function apiPathSegments(urlOrPath) {
452
+ const isUrl = URL.canParse(urlOrPath);
453
+ const path = isUrl ? new URL(urlOrPath).pathname : urlOrPath.split(/[?#]/, 1)[0] ?? "";
454
+ const segments = path.split("/").filter((segment) => segment.length > 0);
455
+ for (let index = 0; index < segments.length - 1; index += 1) {
456
+ if (segments[index] === "api" && segments[index + 1] === "0") {
457
+ return segments.slice(index + 2);
458
+ }
459
+ }
460
+ return isUrl ? void 0 : segments;
461
+ }
462
+ function parseApiPathScope(urlOrPath) {
463
+ const segments = apiPathSegments(urlOrPath);
464
+ if (!segments) {
465
+ return;
466
+ }
467
+ const [resource, organization, next, project] = segments;
468
+ if (!isApiPathIdentifier(organization)) {
469
+ return;
470
+ }
471
+ if (resource === "organizations") {
472
+ return next === "projects" && isApiPathIdentifier(project) ? { organization, project } : { organization };
473
+ }
474
+ if (resource === "projects") {
475
+ return isApiPathIdentifier(next) ? { organization, project: next } : { organization };
476
+ }
477
+ if (resource === "teams") {
478
+ return { organization };
479
+ }
480
+ return;
481
+ }
482
+
446
483
  // src/core/auth.gen.ts
447
484
  var getAuthToken = async (auth, callback) => {
448
485
  const token = typeof callback === "function" ? await callback(auth) : callback;
@@ -677,6 +714,15 @@ var assertFields = (config) => {
677
714
  throw new TypeError('@sentry/api only supports responseStyle: "fields". Read result.data instead.');
678
715
  }
679
716
  };
717
+ var resolveBaseUrl = async (resolve, baseUrl, url) => {
718
+ const scope = parseApiPathScope(url);
719
+ const resolved = await resolve(scope ? { baseUrl, url, scope } : { baseUrl, url });
720
+ let end = resolved.length;
721
+ while (end > 0 && resolved.charAt(end - 1) === "/") {
722
+ end -= 1;
723
+ }
724
+ return resolved.slice(0, end);
725
+ };
680
726
  var createClient = (config = {}) => {
681
727
  assertFields(config);
682
728
  let _config = mergeConfigs(createConfig(), config);
@@ -709,6 +755,13 @@ var createClient = (config = {}) => {
709
755
  opts.headers.delete("Content-Type");
710
756
  }
711
757
  const resolvedOpts = opts;
758
+ if (resolvedOpts.resolveBaseUrl) {
759
+ resolvedOpts.baseUrl = await resolveBaseUrl(
760
+ resolvedOpts.resolveBaseUrl,
761
+ resolvedOpts.baseUrl ?? "",
762
+ buildUrl({ ...resolvedOpts, baseUrl: "" })
763
+ );
764
+ }
712
765
  const url = buildUrl(resolvedOpts);
713
766
  return { opts: resolvedOpts, url };
714
767
  };