@sentry/api 1.28.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
  };
@@ -21309,7 +21309,7 @@ export declare const vListOrganizationStatsV2Path: v.ObjectSchema<{
21309
21309
  readonly organization_id_or_slug: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DescriptionAction<string, "The ID or slug of the organization the resource belongs to.">]>;
21310
21310
  }, undefined>;
21311
21311
  export declare const vListOrganizationStatsV2Query: v.ObjectSchema<{
21312
- readonly groupBy: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.PicklistSchema<["outcome", "category", "reason", "project"], undefined>, v.DescriptionAction<"outcome" | "category" | "reason" | "project", "* `outcome`\n* `category`\n* `reason`\n* `project`">]>, undefined>, v.DescriptionAction<("outcome" | "category" | "reason" | "project")[], "can pass multiple groupBy parameters to group by multiple, e.g. `groupBy=project&groupBy=outcome` to group by multiple dimensions. Note that grouping by project can cause missing rows if the number of projects / interval is large. If you have a large number of projects, we recommend filtering and querying by them individually.Also note that grouping by projects does not currently support timeseries interval responses and will instead be a sum of the projectover the entire period specified.">]>;
21312
+ readonly groupBy: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.PicklistSchema<["outcome", "category", "reason", "project"], undefined>, v.DescriptionAction<"project" | "outcome" | "category" | "reason", "* `outcome`\n* `category`\n* `reason`\n* `project`">]>, undefined>, v.DescriptionAction<("project" | "outcome" | "category" | "reason")[], "can pass multiple groupBy parameters to group by multiple, e.g. `groupBy=project&groupBy=outcome` to group by multiple dimensions. Note that grouping by project can cause missing rows if the number of projects / interval is large. If you have a large number of projects, we recommend filtering and querying by them individually.Also note that grouping by projects does not currently support timeseries interval responses and will instead be a sum of the projectover the entire period specified.">]>;
21313
21313
  readonly field: v.SchemaWithPipe<readonly [v.PicklistSchema<["sum(quantity)", "sum(times_seen)"], undefined>, v.DescriptionAction<"sum(quantity)" | "sum(times_seen)", "the `sum(quantity)` field is bytes for attachments, and all others the 'event' count for those types of events.\n\n`sum(times_seen)` sums the number of times an event has been seen. For 'normal' event types, this will be equal to `sum(quantity)` for now. For sessions, quantity will sum the total number of events seen in a session, while `times_seen` will be the unique number of sessions. and for attachments, `times_seen` will be the total number of attachments, while quantity will be the total sum of attachment bytes.\n\n* `sum(quantity)`\n* `sum(times_seen)`">]>;
21314
21314
  readonly statsPeriod: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, v.DescriptionAction<string, "This defines the range of the time series, relative to now. The range is given in a `<number><unit>` format. For example `1d` for a one day range. Possible units are `m` for minutes, `h` for hours, `d` for days and `w` for weeks. You must either provide a `statsPeriod`, or a `start` and `end`.">]>, undefined>;
21315
21315
  readonly interval: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, v.DescriptionAction<string, "This is the resolution of the time series, given in the same format as `statsPeriod`. The default resolution is `1h` and the minimum resolution is currently restricted to `1h` as well. Intervals larger than `1d` are not supported, and the interval has to cleanly divide one day.">]>, undefined>;
@@ -31937,7 +31937,7 @@ export declare const vListOrganizationStatsV2Data: v.ObjectSchema<{
31937
31937
  readonly organization_id_or_slug: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.DescriptionAction<string, "The ID or slug of the organization the resource belongs to.">]>;
31938
31938
  }, undefined>;
31939
31939
  readonly query: v.ObjectSchema<{
31940
- readonly groupBy: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.PicklistSchema<["outcome", "category", "reason", "project"], undefined>, v.DescriptionAction<"outcome" | "category" | "reason" | "project", "* `outcome`\n* `category`\n* `reason`\n* `project`">]>, undefined>, v.DescriptionAction<("outcome" | "category" | "reason" | "project")[], "can pass multiple groupBy parameters to group by multiple, e.g. `groupBy=project&groupBy=outcome` to group by multiple dimensions. Note that grouping by project can cause missing rows if the number of projects / interval is large. If you have a large number of projects, we recommend filtering and querying by them individually.Also note that grouping by projects does not currently support timeseries interval responses and will instead be a sum of the projectover the entire period specified.">]>;
31940
+ readonly groupBy: v.SchemaWithPipe<readonly [v.ArraySchema<v.SchemaWithPipe<readonly [v.PicklistSchema<["outcome", "category", "reason", "project"], undefined>, v.DescriptionAction<"project" | "outcome" | "category" | "reason", "* `outcome`\n* `category`\n* `reason`\n* `project`">]>, undefined>, v.DescriptionAction<("project" | "outcome" | "category" | "reason")[], "can pass multiple groupBy parameters to group by multiple, e.g. `groupBy=project&groupBy=outcome` to group by multiple dimensions. Note that grouping by project can cause missing rows if the number of projects / interval is large. If you have a large number of projects, we recommend filtering and querying by them individually.Also note that grouping by projects does not currently support timeseries interval responses and will instead be a sum of the projectover the entire period specified.">]>;
31941
31941
  readonly field: v.SchemaWithPipe<readonly [v.PicklistSchema<["sum(quantity)", "sum(times_seen)"], undefined>, v.DescriptionAction<"sum(quantity)" | "sum(times_seen)", "the `sum(quantity)` field is bytes for attachments, and all others the 'event' count for those types of events.\n\n`sum(times_seen)` sums the number of times an event has been seen. For 'normal' event types, this will be equal to `sum(quantity)` for now. For sessions, quantity will sum the total number of events seen in a session, while `times_seen` will be the unique number of sessions. and for attachments, `times_seen` will be the total number of attachments, while quantity will be the total sum of attachment bytes.\n\n* `sum(quantity)`\n* `sum(times_seen)`">]>;
31942
31942
  readonly statsPeriod: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, v.DescriptionAction<string, "This defines the range of the time series, relative to now. The range is given in a `<number><unit>` format. For example `1d` for a one day range. Possible units are `m` for minutes, `h` for hours, `d` for days and `w` for weeks. You must either provide a `statsPeriod`, or a `start` and `end`.">]>, undefined>;
31943
31943
  readonly interval: v.OptionalSchema<v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, v.DescriptionAction<string, "This is the resolution of the time series, given in the same format as `statsPeriod`. The default resolution is `1h` and the minimum resolution is currently restricted to `1h` as well. Intervals larger than `1d` are not supported, and the interval has to cleanly divide one day.">]>, undefined>;
package/dist/zod.gen.d.ts CHANGED
@@ -23962,10 +23962,10 @@ export declare const zListOrganizationStatsV2Path: z.ZodObject<{
23962
23962
  }, z.core.$strip>;
23963
23963
  export declare const zListOrganizationStatsV2Query: z.ZodObject<{
23964
23964
  groupBy: z.ZodArray<z.ZodEnum<{
23965
+ project: "project";
23965
23966
  outcome: "outcome";
23966
23967
  category: "category";
23967
23968
  reason: "reason";
23968
- project: "project";
23969
23969
  }>>;
23970
23970
  field: z.ZodEnum<{
23971
23971
  "sum(quantity)": "sum(quantity)";
@@ -39682,10 +39682,10 @@ export declare const zListOrganizationStatsV2Data: z.ZodObject<{
39682
39682
  }, z.core.$strip>;
39683
39683
  query: z.ZodObject<{
39684
39684
  groupBy: z.ZodArray<z.ZodEnum<{
39685
+ project: "project";
39685
39686
  outcome: "outcome";
39686
39687
  category: "category";
39687
39688
  reason: "reason";
39688
- project: "project";
39689
39689
  }>>;
39690
39690
  field: z.ZodEnum<{
39691
39691
  "sum(quantity)": "sum(quantity)";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/api",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "Official auto-generated TypeScript client for the Sentry public REST API",
5
5
  "keywords": [
6
6
  "sentry",