@sentry/api 0.445.0 → 0.446.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.
Files changed (39) hide show
  1. package/README.md +28 -0
  2. package/dist/api-client/api-path.d.ts +34 -0
  3. package/dist/api-client/auth-token.d.ts +6 -0
  4. package/dist/api-client/core.d.ts +84 -0
  5. package/dist/api-client/endpoints/client-keys.d.ts +38 -0
  6. package/dist/api-client/endpoints/dashboards.d.ts +23 -0
  7. package/dist/api-client/endpoints/index.d.ts +18 -0
  8. package/dist/api-client/endpoints/index.js +762 -0
  9. package/dist/api-client/endpoints/monitors.d.ts +49 -0
  10. package/dist/api-client/endpoints/organizations.d.ts +35 -0
  11. package/dist/api-client/endpoints/profiling.d.ts +32 -0
  12. package/dist/api-client/endpoints/projects.d.ts +63 -0
  13. package/dist/api-client/endpoints/releases.d.ts +45 -0
  14. package/dist/api-client/endpoints/replays.d.ts +53 -0
  15. package/dist/api-client/endpoints/repositories.d.ts +31 -0
  16. package/dist/api-client/endpoints/snapshots.d.ts +29 -0
  17. package/dist/api-client/endpoints/teams.d.ts +23 -0
  18. package/dist/api-client/endpoints/traces.d.ts +25 -0
  19. package/dist/api-client/endpoints/uptime-monitors.d.ts +61 -0
  20. package/dist/api-client/errors.d.ts +151 -0
  21. package/dist/api-client/idempotent-operations.d.ts +33 -0
  22. package/dist/api-client/index.d.ts +20 -0
  23. package/dist/api-client/index.js +1647 -0
  24. package/dist/api-client/pagination.d.ts +119 -0
  25. package/dist/api-client/response-cache/body.d.ts +31 -0
  26. package/dist/api-client/response-cache/index.d.ts +14 -0
  27. package/dist/api-client/response-cache/invalidation.d.ts +42 -0
  28. package/dist/api-client/response-cache/keys.d.ts +25 -0
  29. package/dist/api-client/response-cache/memory-store.d.ts +37 -0
  30. package/dist/api-client/response-cache/policy.d.ts +81 -0
  31. package/dist/api-client/response-cache/store.d.ts +52 -0
  32. package/dist/api-client/response-cache/tiers.d.ts +28 -0
  33. package/dist/api-client/response.d.ts +27 -0
  34. package/dist/api-client/retry.d.ts +57 -0
  35. package/dist/api-client/standard-schema.d.ts +65 -0
  36. package/dist/api-client/time-params.d.ts +46 -0
  37. package/dist/api-client/transport.d.ts +156 -0
  38. package/dist/api-client/validate.d.ts +22 -0
  39. package/package.json +15 -2
@@ -0,0 +1,1647 @@
1
+ // src/api-client/errors.ts
2
+ import { SentryApiError } from "@sentry/api";
3
+
4
+ class UserInputError extends Error {
5
+ constructor(message, options) {
6
+ super(message, options);
7
+ this.name = "UserInputError";
8
+ }
9
+ }
10
+
11
+ class ConfigurationError extends Error {
12
+ constructor(message, options) {
13
+ super(message, options);
14
+ this.name = "ConfigurationError";
15
+ }
16
+ }
17
+
18
+ class ApiError extends SentryApiError {
19
+ detail;
20
+ responseBody;
21
+ constructor(message, status, detail, responseBody) {
22
+ super(status, responseBody, undefined, false, message);
23
+ this.detail = detail;
24
+ this.responseBody = responseBody;
25
+ this.name = "ApiError";
26
+ Object.setPrototypeOf(this, ApiError.prototype);
27
+ }
28
+ }
29
+
30
+ class ApiClientError extends ApiError {
31
+ constructor(message, status, detail, responseBody) {
32
+ super(message, status, detail, responseBody);
33
+ this.name = "ApiClientError";
34
+ Object.setPrototypeOf(this, ApiClientError.prototype);
35
+ }
36
+ toUserMessage() {
37
+ const baseMessage = this.status ? `API error (${this.status}): ${this.message}` : `API error: ${this.message}`;
38
+ if (this.status === 404) {
39
+ const genericMessages = [
40
+ "not found",
41
+ "the requested resource does not exist",
42
+ "resource does not exist",
43
+ "resource not found"
44
+ ];
45
+ const isGeneric = genericMessages.some((msg) => this.message.toLowerCase() === msg || this.message.toLowerCase().includes("requested resource"));
46
+ if (isGeneric) {
47
+ return `${baseMessage}. Please verify that the organization, project, or resource ID is correct and that you have access to it.`;
48
+ }
49
+ return `${baseMessage}. Please verify the parameters are correct.`;
50
+ }
51
+ return baseMessage;
52
+ }
53
+ isPermissionError() {
54
+ return this.status === 403;
55
+ }
56
+ isNotFoundError() {
57
+ return this.status === 404;
58
+ }
59
+ isValidationError() {
60
+ return this.status === 400 || this.status === 422;
61
+ }
62
+ isAuthenticationError() {
63
+ return this.status === 401;
64
+ }
65
+ isRateLimitError() {
66
+ return this.status === 429;
67
+ }
68
+ }
69
+
70
+ class ApiPermissionError extends ApiClientError {
71
+ constructor(message, detail, responseBody) {
72
+ super(message, 403, detail, responseBody);
73
+ this.name = "ApiPermissionError";
74
+ Object.setPrototypeOf(this, ApiPermissionError.prototype);
75
+ }
76
+ isMultiProjectAccessError() {
77
+ return this.message.includes("multiple projects") || this.message.includes("multi project") || this.message.includes("multi-project") || this.message.includes("You do not have access to query across multiple projects");
78
+ }
79
+ }
80
+
81
+ class ApiNotFoundError extends ApiClientError {
82
+ resourceType;
83
+ resourceId;
84
+ constructor(message, detail, responseBody, resourceType, resourceId) {
85
+ super(message, 404, detail, responseBody);
86
+ this.resourceType = resourceType;
87
+ this.resourceId = resourceId;
88
+ this.name = "ApiNotFoundError";
89
+ Object.setPrototypeOf(this, ApiNotFoundError.prototype);
90
+ }
91
+ }
92
+
93
+ class ApiValidationError extends ApiClientError {
94
+ validationErrors;
95
+ constructor(message, status, detail, responseBody, validationErrors) {
96
+ super(message, status, detail, responseBody);
97
+ this.validationErrors = validationErrors;
98
+ this.name = "ApiValidationError";
99
+ Object.setPrototypeOf(this, ApiValidationError.prototype);
100
+ }
101
+ }
102
+
103
+ class ApiAuthenticationError extends ApiClientError {
104
+ constructor(message, detail, responseBody) {
105
+ super(message, 401, detail, responseBody);
106
+ this.name = "ApiAuthenticationError";
107
+ Object.setPrototypeOf(this, ApiAuthenticationError.prototype);
108
+ }
109
+ }
110
+
111
+ class ApiRateLimitError extends ApiClientError {
112
+ retryAfterMs;
113
+ constructor(message, detail, responseBody, retryAfterMs) {
114
+ super(message, 429, detail, responseBody);
115
+ this.retryAfterMs = retryAfterMs;
116
+ this.name = "ApiRateLimitError";
117
+ Object.setPrototypeOf(this, ApiRateLimitError.prototype);
118
+ }
119
+ toUserMessage() {
120
+ if (this.retryAfterMs === undefined) {
121
+ return `${super.toUserMessage()}. Retry in a few seconds.`;
122
+ }
123
+ const seconds = Math.max(1, Math.ceil(this.retryAfterMs / 1000));
124
+ return `${super.toUserMessage()}. Retry in ${seconds} second${seconds === 1 ? "" : "s"}.`;
125
+ }
126
+ }
127
+
128
+ class ApiServerError extends ApiError {
129
+ constructor(message, status, detail, responseBody) {
130
+ super(message, status, detail, responseBody);
131
+ this.name = "ApiServerError";
132
+ Object.setPrototypeOf(this, ApiServerError.prototype);
133
+ }
134
+ isGatewayError() {
135
+ return this.status === 502 || this.status === 503 || this.status === 504;
136
+ }
137
+ isInternalError() {
138
+ return this.status === 500;
139
+ }
140
+ }
141
+ function createApiError(message, status, detail, responseBody, retryAfterMs) {
142
+ let improvedMessage = message;
143
+ if (message.includes("You do not have the multi project stream feature enabled") || message.includes("You cannot view events from multiple projects")) {
144
+ improvedMessage = "You do not have access to query across multiple projects. Please select a project for your query.";
145
+ return new ApiPermissionError(improvedMessage, detail, responseBody);
146
+ }
147
+ switch (status) {
148
+ case 401:
149
+ return new ApiAuthenticationError(message, detail, responseBody);
150
+ case 403:
151
+ return new ApiPermissionError(message, detail, responseBody);
152
+ case 404:
153
+ return new ApiNotFoundError(message, detail, responseBody);
154
+ case 400:
155
+ case 422: {
156
+ let validationErrors;
157
+ if (responseBody && typeof responseBody === "object" && "errors" in responseBody) {
158
+ validationErrors = responseBody.errors;
159
+ }
160
+ return new ApiValidationError(message, status, detail, responseBody, validationErrors);
161
+ }
162
+ case 429:
163
+ return new ApiRateLimitError(message, detail, responseBody, retryAfterMs);
164
+ default:
165
+ if (status >= 400 && status < 500) {
166
+ return new ApiClientError(message, status, detail, responseBody);
167
+ }
168
+ if (status >= 500 && status < 600) {
169
+ return new ApiServerError(message, status, detail, responseBody);
170
+ }
171
+ return new ApiError(message, status, detail, responseBody);
172
+ }
173
+ }
174
+ function isApiAuthenticationErrorDeep(error) {
175
+ let current = error;
176
+ for (let i = 0;i < 3; i++) {
177
+ if (current instanceof ApiAuthenticationError)
178
+ return true;
179
+ if (!(current instanceof Error))
180
+ return false;
181
+ current = current.cause;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ // src/api-client/api-path.ts
187
+ function encodeApiPathSegment(value) {
188
+ const text = String(value);
189
+ return text === "." || text === ".." ? null : encodeURIComponent(text);
190
+ }
191
+ function buildSentryApiUrl(baseUrl, endpoint) {
192
+ const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
193
+ let end = baseUrl.length;
194
+ while (end > 0 && baseUrl.charAt(end - 1) === "/") {
195
+ end -= 1;
196
+ }
197
+ return `${baseUrl.slice(0, end)}/api/0/${path}`;
198
+ }
199
+ function apiPath(strings, ...values) {
200
+ return strings.reduce((acc, literal, index) => {
201
+ if (index >= values.length) {
202
+ return acc + literal;
203
+ }
204
+ const value = String(values[index]);
205
+ const encoded = encodeApiPathSegment(value);
206
+ if (encoded === null) {
207
+ throw new UserInputError(`Invalid identifier "${value}": relative path segments are not allowed.`);
208
+ }
209
+ return acc + literal + encoded;
210
+ }, "");
211
+ }
212
+ function withQuery(path, query) {
213
+ const queryString = query.toString();
214
+ return queryString ? `${path}?${queryString}` : path;
215
+ }
216
+ // src/api-client/auth-token.ts
217
+ var INVALID_TOKEN_CHARACTER_PATTERN = /[^\x21-\x7e]/;
218
+ var TOKEN_PADDING_PATTERN = /[\s\x00-\x1f\x7f]/;
219
+ function trimAuthToken(token) {
220
+ let start = 0;
221
+ let end = token.length;
222
+ while (start < end && TOKEN_PADDING_PATTERN.test(token.charAt(start))) {
223
+ start += 1;
224
+ }
225
+ while (end > start && TOKEN_PADDING_PATTERN.test(token.charAt(end - 1))) {
226
+ end -= 1;
227
+ }
228
+ return token.slice(start, end);
229
+ }
230
+ function normalizeAuthToken(token) {
231
+ const normalized = trimAuthToken(token);
232
+ return normalized && !INVALID_TOKEN_CHARACTER_PATTERN.test(normalized) ? normalized : null;
233
+ }
234
+ function sentryBearerHeader(token) {
235
+ const normalized = normalizeAuthToken(token);
236
+ return normalized === null ? null : `Bearer ${normalized}`;
237
+ }
238
+ // src/api-client/core.ts
239
+ import {
240
+ createSentryClient,
241
+ parseSentryLinkHeader
242
+ } from "@sentry/api";
243
+
244
+ // src/api-client/pagination.ts
245
+ import { consumePage, paginateUpTo } from "@sentry/api";
246
+ var API_MAX_PER_PAGE = 100;
247
+ var NEXT_CURSOR_RE = /^(.*:\d+):(0(?:_?0)*)$/;
248
+ var FIRST_PAGE_CURSOR = "0:0:0";
249
+ var PAGE_NUMBER_CURSOR_RE = /^(\d+):\d+:[01]$/;
250
+ var CLI_CURSOR_PREFIX = "cli1~";
251
+ function encodeSkip(skip) {
252
+ return `0${skip.toString(2).replaceAll("1", "_0")}`;
253
+ }
254
+ function decodeSkip(isPrev) {
255
+ return Number.parseInt(isPrev.slice(1).replaceAll("_0", "1"), 2) || 0;
256
+ }
257
+ function encodePageCursor(cursor, skip) {
258
+ const server = cursor ?? FIRST_PAGE_CURSOR;
259
+ const next = skip > 0 ? NEXT_CURSOR_RE.exec(server) : null;
260
+ return next ? `${next[1]}:${encodeSkip(skip)}` : server;
261
+ }
262
+ function decodePageCursor(raw, warn = () => {}) {
263
+ if (!raw) {
264
+ return { cursor: undefined, skip: 0 };
265
+ }
266
+ if (raw.startsWith(CLI_CURSOR_PREFIX)) {
267
+ return decodeCliCursor(raw, warn);
268
+ }
269
+ const next = NEXT_CURSOR_RE.exec(raw);
270
+ const cursor = next ? `${next[1]}:0` : raw;
271
+ const position = {
272
+ cursor: cursor === FIRST_PAGE_CURSOR ? undefined : cursor,
273
+ skip: next?.[2] ? decodeSkip(next[2]) : 0
274
+ };
275
+ const perPage = serverCursorPageSize(cursor);
276
+ return perPage === undefined ? position : { ...position, perPage };
277
+ }
278
+ function decodeCliCursor(raw, warn) {
279
+ const [perPage, skip, ...server] = raw.slice(CLI_CURSOR_PREFIX.length).split("~");
280
+ const position = {
281
+ perPage: Number(perPage),
282
+ skip: Number(skip),
283
+ cursor: server.join("~") || undefined
284
+ };
285
+ if (Number.isInteger(position.perPage) && position.perPage >= 1 && position.perPage <= API_MAX_PER_PAGE && Number.isInteger(position.skip) && position.skip >= 0) {
286
+ return position;
287
+ }
288
+ warn("Ignoring an unreadable pagination cursor; starting from the first page.");
289
+ return { cursor: undefined, skip: 0 };
290
+ }
291
+ function consumeCursorPage(page, position, limit = Number.POSITIVE_INFINITY) {
292
+ const consumed = consumePage({ data: page.data, nextCursor: page.nextCursor || undefined }, { cursor: position.cursor, offset: position.skip }, limit);
293
+ return {
294
+ ...page,
295
+ data: consumed.data,
296
+ nextCursor: consumed.position ? encodePageCursor(consumed.position.cursor, consumed.position.offset) : undefined
297
+ };
298
+ }
299
+ function serverCursorPageSize(cursor) {
300
+ const value = Number(PAGE_NUMBER_CURSOR_RE.exec(cursor)?.[1]);
301
+ return Number.isInteger(value) && value >= 1 && value <= API_MAX_PER_PAGE ? value : undefined;
302
+ }
303
+ function pageSizeForLimit(limit) {
304
+ if (limit <= API_MAX_PER_PAGE) {
305
+ return Math.max(1, limit);
306
+ }
307
+ return Math.ceil(limit / Math.ceil(limit / API_MAX_PER_PAGE));
308
+ }
309
+ async function paginate(options, fetchPage) {
310
+ const { limit, maxPages, warn = () => {} } = options;
311
+ const position = decodePageCursor(options.cursor, warn);
312
+ const perPage = position.perPage ?? pageSizeForLimit(limit + position.skip);
313
+ const result = await paginateUpTo(async (cursor) => {
314
+ const page = await fetchPage(perPage, cursor);
315
+ return { data: page.data, nextCursor: page.nextCursor || undefined };
316
+ }, {
317
+ limit,
318
+ maxPages,
319
+ resume: {
320
+ cursor: position.cursor,
321
+ offset: position.skip,
322
+ requestBudget: limit + position.skip
323
+ }
324
+ }, "paginate");
325
+ return {
326
+ data: result.data,
327
+ ...result.continuation ? {
328
+ nextCursor: encodePageCursor(result.continuation.cursor, result.continuation.offset)
329
+ } : {},
330
+ stoppedAtPageCap: result.stopReason === "maxPages"
331
+ };
332
+ }
333
+ async function fetchAllPages(fetchPage, parse, maxPages) {
334
+ const items = [];
335
+ const seen = new Set;
336
+ let cursor;
337
+ for (let page = 0;page < maxPages; page += 1) {
338
+ const { data, nextCursor } = await fetchPage(cursor);
339
+ const rows = parse(data);
340
+ if (!rows) {
341
+ return { ok: false, reason: "invalid-page" };
342
+ }
343
+ items.push(...rows);
344
+ if (!nextCursor) {
345
+ return { ok: true, items };
346
+ }
347
+ if (seen.has(nextCursor)) {
348
+ return { ok: false, reason: "repeated-cursor" };
349
+ }
350
+ seen.add(nextCursor);
351
+ cursor = nextCursor;
352
+ }
353
+ return { ok: false, reason: "page-cap" };
354
+ }
355
+
356
+ // src/api-client/retry.ts
357
+ var IDEMPOTENT_METHODS = new Set([
358
+ "GET",
359
+ "HEAD",
360
+ "OPTIONS"
361
+ ]);
362
+ var RETRYABLE_STATUSES = new Map([
363
+ [429, "any"],
364
+ [500, "idempotent"],
365
+ [502, "idempotent"],
366
+ [503, "idempotent"],
367
+ [504, "idempotent"]
368
+ ]);
369
+ var DATE_RESOLUTION_MS = 1000;
370
+ function isIdempotentRequest(method, idempotent = false) {
371
+ return idempotent || IDEMPOTENT_METHODS.has(method.toUpperCase());
372
+ }
373
+ function isRetryable(method, status, idempotent = false) {
374
+ const replays = status === undefined ? "idempotent" : RETRYABLE_STATUSES.get(status);
375
+ if (replays === undefined) {
376
+ return false;
377
+ }
378
+ return replays === "any" || isIdempotentRequest(method, idempotent);
379
+ }
380
+ function parseRetryDelayMs(status, headers, now = Date.now()) {
381
+ const retryAfter = headers.get("retry-after")?.trim();
382
+ if (retryAfter) {
383
+ if (/^\d+$/.test(retryAfter)) {
384
+ return Number(retryAfter) * 1000;
385
+ }
386
+ const date = Date.parse(retryAfter);
387
+ return Number.isNaN(date) ? undefined : delayUntil(date, headers, now);
388
+ }
389
+ if (status !== 429 || !isWindowLimited(headers)) {
390
+ return;
391
+ }
392
+ const reset = Number(headers.get("x-sentry-rate-limit-reset"));
393
+ if (!Number.isFinite(reset) || reset <= 0) {
394
+ return;
395
+ }
396
+ return delayUntil(reset * 1000, headers, now);
397
+ }
398
+ function nextRetryDelayMs({
399
+ policy,
400
+ method,
401
+ idempotent,
402
+ status,
403
+ serverDelayMs,
404
+ attempt,
405
+ waitedMs,
406
+ random = Math.random
407
+ }) {
408
+ if (attempt >= policy.maxRetries || !isRetryable(method, status, idempotent)) {
409
+ return;
410
+ }
411
+ let delayMs;
412
+ if (serverDelayMs !== undefined) {
413
+ if (serverDelayMs > policy.maxDelayMs) {
414
+ return;
415
+ }
416
+ delayMs = serverDelayMs + random() * (policy.baseDelayMs / 2);
417
+ } else {
418
+ const step = Math.min(policy.baseDelayMs * 2 ** attempt, policy.maxDelayMs);
419
+ delayMs = step / 2 + random() * (step / 2);
420
+ }
421
+ return waitedMs + delayMs > policy.maxTotalDelayMs ? undefined : delayMs;
422
+ }
423
+ function sleep(ms, signal) {
424
+ return new Promise((resolve, reject) => {
425
+ if (signal?.aborted) {
426
+ reject(signal.reason);
427
+ return;
428
+ }
429
+ const onAbort = () => {
430
+ clearTimeout(timer);
431
+ reject(signal?.reason);
432
+ };
433
+ const timer = setTimeout(() => {
434
+ signal?.removeEventListener("abort", onAbort);
435
+ resolve();
436
+ }, ms);
437
+ signal?.addEventListener("abort", onAbort, { once: true });
438
+ });
439
+ }
440
+ function delayUntil(target, headers, now) {
441
+ let delay = target - now;
442
+ const date = Date.parse(headers.get("date") ?? "");
443
+ if (!Number.isNaN(date)) {
444
+ const latest = target - date;
445
+ delay = Math.min(Math.max(delay, latest - DATE_RESOLUTION_MS), latest);
446
+ }
447
+ return Math.max(0, delay);
448
+ }
449
+ function isWindowLimited(headers) {
450
+ return headers.get("x-sentry-rate-limit-remaining") === "0";
451
+ }
452
+
453
+ // src/api-client/response.ts
454
+ function readDetail(json) {
455
+ if (json === null || typeof json !== "object" || !("detail" in json)) {
456
+ return;
457
+ }
458
+ return typeof json.detail === "string" ? json.detail : undefined;
459
+ }
460
+ function looksLikeHtml(text) {
461
+ return text.includes("<!DOCTYPE") || text.includes("<html");
462
+ }
463
+ async function readErrorResponse(response) {
464
+ const retryAfterMs = parseRetryDelayMs(response.status, response.headers);
465
+ let text;
466
+ try {
467
+ text = await response.text();
468
+ } catch {
469
+ return { text: undefined, isHtml: false, retryAfterMs };
470
+ }
471
+ try {
472
+ const json = JSON.parse(text);
473
+ return {
474
+ text,
475
+ json,
476
+ detail: readDetail(json),
477
+ isHtml: false,
478
+ retryAfterMs
479
+ };
480
+ } catch (error) {
481
+ return {
482
+ text,
483
+ parseError: error instanceof Error ? error.message : String(error),
484
+ isHtml: looksLikeHtml(text),
485
+ retryAfterMs
486
+ };
487
+ }
488
+ }
489
+ async function readJsonResponse(response) {
490
+ if (!response.headers?.get) {
491
+ return await response.json();
492
+ }
493
+ const contentType = response.headers.get("content-type");
494
+ if (!contentType?.includes("application/json")) {
495
+ const text = await response.text();
496
+ if (contentType?.includes("text/html") || looksLikeHtml(text)) {
497
+ throw new Error(`Expected JSON response but received HTML (${response.status} ${response.statusText}). This may indicate you're not authenticated, the URL is incorrect, or there's a server issue.`);
498
+ }
499
+ throw new Error(`Expected JSON response but received ${contentType || "unknown content type"} ` + `(${response.status} ${response.statusText})`);
500
+ }
501
+ try {
502
+ return await response.json();
503
+ } catch (error) {
504
+ throw new Error(`Failed to parse JSON response: ${error instanceof Error ? error.message : String(error)}`);
505
+ }
506
+ }
507
+
508
+ // src/api-client/validate.ts
509
+ var MAX_LISTED_ISSUES = 3;
510
+ function issuePath(issue) {
511
+ return (issue.path ?? []).map((segment) => String(typeof segment === "object" ? segment.key : segment)).join(".");
512
+ }
513
+
514
+ class ApiResponseValidationError extends Error {
515
+ issues;
516
+ constructor(issues) {
517
+ const listed = issues.slice(0, MAX_LISTED_ISSUES).map((issue) => {
518
+ const path = issuePath(issue);
519
+ return path ? `${path}: ${issue.message}` : issue.message;
520
+ });
521
+ const more = issues.length > MAX_LISTED_ISSUES ? ` (and ${issues.length - MAX_LISTED_ISSUES} more)` : "";
522
+ super(`Unexpected API response: ${listed.join("; ")}${more}`);
523
+ this.name = "ApiResponseValidationError";
524
+ this.issues = issues;
525
+ }
526
+ }
527
+ function validateResponse(schema, data) {
528
+ const result = schema["~standard"].validate(data);
529
+ if (result instanceof Promise) {
530
+ throw new TypeError("Response schemas must validate synchronously");
531
+ }
532
+ if (result.issues) {
533
+ throw new ApiResponseValidationError(result.issues);
534
+ }
535
+ return result.value;
536
+ }
537
+
538
+ // src/api-client/core.ts
539
+ var MAX_LIST_PAGES = 50;
540
+ function nextPageCursor(response) {
541
+ return parseSentryLinkHeader(response.headers.get("link")).nextCursor ?? null;
542
+ }
543
+ var NETWORK_ERROR_MESSAGES = {
544
+ EAI_AGAIN: "DNS temporarily unavailable. Check your internet connection.",
545
+ ENOTFOUND: "Hostname not found. Verify the URL is correct.",
546
+ ECONNREFUSED: "Connection refused. Ensure the service is accessible.",
547
+ ETIMEDOUT: "Connection timed out. Check network connectivity.",
548
+ ECONNRESET: "Connection reset. Try again in a moment."
549
+ };
550
+ var API_PREFIX = "/api/0";
551
+ var CONFIGURATION_ERROR_CODES = new Set([
552
+ "ENOTFOUND",
553
+ "EAI_AGAIN",
554
+ "ECONNREFUSED",
555
+ "ETIMEDOUT"
556
+ ]);
557
+ function networkError(url, error) {
558
+ let rootCause = error;
559
+ while (rootCause instanceof Error && rootCause.cause) {
560
+ rootCause = rootCause.cause;
561
+ }
562
+ const errorMessage = rootCause instanceof Error ? rootCause.message : String(rootCause);
563
+ const errorCode = Object.keys(NETWORK_ERROR_MESSAGES).find((code) => errorMessage.includes(code));
564
+ const friendlyMessage = `Unable to connect to ${url} - ${errorCode ? NETWORK_ERROR_MESSAGES[errorCode] : errorMessage}`;
565
+ if (errorCode && CONFIGURATION_ERROR_CODES.has(errorCode) || errorMessage.includes("Connect Timeout Error")) {
566
+ return new ConfigurationError(friendlyMessage, { cause: error });
567
+ }
568
+ return new Error(friendlyMessage, { cause: error });
569
+ }
570
+ function errorFromResponse(logger, response, body, logExtra) {
571
+ const { retryAfterMs } = body;
572
+ const errorText = body.text ?? "";
573
+ const bodyPreview = errorText.length > 256 ? `${errorText.slice(0, 253)}…` : errorText;
574
+ if (body.parseError !== undefined) {
575
+ if (body.isHtml) {
576
+ logger.warn("Received HTML error page instead of JSON", {
577
+ ...logExtra,
578
+ parseErrorMessage: body.parseError
579
+ });
580
+ return createApiError(`Server error: Received HTML instead of JSON (${response.status} ${response.statusText}). This may indicate an invalid URL or server issue.`, response.status, errorText, undefined, retryAfterMs);
581
+ }
582
+ logger.warn("Failed to parse JSON error response", {
583
+ ...logExtra,
584
+ bodyPreview,
585
+ parseErrorMessage: body.parseError
586
+ });
587
+ } else if (body.json) {
588
+ if (body.detail !== undefined) {
589
+ return createApiError(body.detail, response.status, body.detail, body.json, retryAfterMs);
590
+ }
591
+ logger.warn("Failed to parse validated API error response", {
592
+ ...logExtra,
593
+ bodyPreview,
594
+ validationErrorMessage: "Error response has no string detail"
595
+ });
596
+ }
597
+ return createApiError(`API request failed: ${response.statusText}
598
+ ${errorText}`, response.status, errorText, undefined, retryAfterMs);
599
+ }
600
+ function createApiClientCore({
601
+ transport,
602
+ headers,
603
+ logger
604
+ }) {
605
+ async function request(origin, path, init = {}, { allowStatuses } = {}) {
606
+ const url = buildSentryApiUrl(origin, path);
607
+ const requestHeaders = {
608
+ "Content-Type": "application/json",
609
+ ...headers()
610
+ };
611
+ if (typeof globalThis.fetch === "undefined") {
612
+ throw new ConfigurationError("fetch is not available. Please use Node.js >= 18 or ensure fetch is available in your environment.");
613
+ }
614
+ let response;
615
+ try {
616
+ response = await transport.request(url, {
617
+ ...init,
618
+ headers: requestHeaders
619
+ });
620
+ } catch (error) {
621
+ if (init.signal?.aborted) {
622
+ throw error;
623
+ }
624
+ throw networkError(url, error);
625
+ }
626
+ if (response.ok || allowStatuses?.includes(response.status)) {
627
+ return response;
628
+ }
629
+ throw errorFromResponse(logger, response, await readErrorResponse(response), {
630
+ status: response.status,
631
+ statusText: response.statusText,
632
+ host: new URL(origin).host,
633
+ path
634
+ });
635
+ }
636
+ return {
637
+ request,
638
+ async requestJson(origin, path, init) {
639
+ return await readJsonResponse(await request(origin, path, init));
640
+ },
641
+ sdkClient(origin, expectedPath) {
642
+ const base = new URL(origin).origin;
643
+ const sdkFetch = async (sdkRequest) => {
644
+ if (!(sdkRequest instanceof Request)) {
645
+ throw new ConfigurationError("Unexpected SDK request input");
646
+ }
647
+ const url = new URL(sdkRequest.url);
648
+ if (url.origin !== base || url.pathname !== API_PREFIX.concat(expectedPath) || sdkRequest.method !== "GET") {
649
+ throw new ConfigurationError("Unexpected SDK request target or method");
650
+ }
651
+ const response = await request(origin, `${url.pathname.slice(API_PREFIX.length)}${url.search}`, { method: "GET", signal: sdkRequest.signal });
652
+ const contentType = response.headers.get("content-type");
653
+ if (!contentType?.includes("application/json")) {
654
+ await readJsonResponse(response);
655
+ }
656
+ return response;
657
+ };
658
+ return createSentryClient({ baseUrl: base, fetch: sdkFetch }).client;
659
+ }
660
+ };
661
+ }
662
+ async function listPage(fetchPage, schema, limit, initialCursor) {
663
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > API_MAX_PER_PAGE * MAX_LIST_PAGES) {
664
+ throw new ApiValidationError(`limit must be an integer between 1 and ${API_MAX_PER_PAGE * MAX_LIST_PAGES}`);
665
+ }
666
+ const items = [];
667
+ const seenCursors = new Set;
668
+ let cursor = initialCursor ?? undefined;
669
+ for (let page = 0;page < MAX_LIST_PAGES; page += 1) {
670
+ const perPage = Math.min(API_MAX_PER_PAGE, limit - items.length);
671
+ const { data, response } = await fetchPage(perPage, cursor);
672
+ const rows = validateResponse(schema, data);
673
+ if (rows.length > perPage) {
674
+ throw new Error("API returned more items than requested");
675
+ }
676
+ items.push(...rows);
677
+ const nextCursor = nextPageCursor(response);
678
+ if (items.length >= limit || !nextCursor) {
679
+ return { items, nextCursor };
680
+ }
681
+ if (seenCursors.has(nextCursor) || nextCursor === cursor) {
682
+ throw new Error("API returned a repeated pagination cursor");
683
+ }
684
+ seenCursors.add(nextCursor);
685
+ cursor = nextCursor;
686
+ }
687
+ throw new Error("API pagination exceeded its page limit");
688
+ }
689
+ // src/api-client/idempotent-operations.ts
690
+ function selectIdempotentWrites(operations) {
691
+ return operations.filter((op) => op.idempotent && !isIdempotentRequest(op.method)).map((op) => ({ method: op.method, path: op.path })).sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
692
+ }
693
+ var TEMPLATE_SEGMENT = /^\{[^/]+\}$/;
694
+ var REGEXP_SPECIAL = /[.*+?^${}()|[\]\\]/g;
695
+ function toMatcher({ method, path }) {
696
+ const source = path.split("/").map((segment) => TEMPLATE_SEGMENT.test(segment) ? "[^/]+" : segment.replace(REGEXP_SPECIAL, "\\$&")).join("/");
697
+ return { method: method.toUpperCase(), pattern: new RegExp(`^${source}$`) };
698
+ }
699
+ function createIdempotentOperationMatcher(operations) {
700
+ let matchers;
701
+ return (method, url) => {
702
+ if (!URL.canParse(url)) {
703
+ return false;
704
+ }
705
+ const pathname = new URL(url).pathname;
706
+ const apiIndex = pathname.indexOf("/api/");
707
+ if (apiIndex === -1) {
708
+ return false;
709
+ }
710
+ const apiPath2 = pathname.slice(apiIndex);
711
+ const normalized = apiPath2.endsWith("/") ? apiPath2 : `${apiPath2}/`;
712
+ const upper = method.toUpperCase();
713
+ matchers ??= operations().map(toMatcher);
714
+ return matchers.some((matcher) => matcher.method === upper && matcher.pattern.test(normalized));
715
+ };
716
+ }
717
+ // src/api-client/response-cache/invalidation.ts
718
+ var API_V0_SEGMENT = "/api/0/";
719
+ var TRAILING_SLASH_RE = /\/$/;
720
+ var SKIP_INVALIDATION_PATTERNS = [
721
+ /\/chunk-upload\//,
722
+ /\/artifactbundle\/assemble\//
723
+ ];
724
+ var CROSS_ENDPOINT_RULES = [
725
+ {
726
+ match: /^teams\/([^/]+)\/[^/]+\/projects\/?$/,
727
+ extra: ([, org]) => [`organizations/${org}/projects/`]
728
+ },
729
+ {
730
+ match: /^projects\/([^/]+)\/[^/]+\/?$/,
731
+ extra: ([, org]) => [`organizations/${org}/projects/`]
732
+ },
733
+ {
734
+ match: /^organizations\/[^/]+\/issues\/([^/]+)\/?$/,
735
+ extraAbsolute: ([, issueId], { apiBaseUrl }) => [
736
+ `${apiBaseUrl.replace(TRAILING_SLASH_RE, "")}/api/0/issues/${issueId}/`
737
+ ]
738
+ }
739
+ ];
740
+ function computeInvalidationPrefixes(fullUrl, apiBaseUrl) {
741
+ let parsed;
742
+ try {
743
+ parsed = new URL(fullUrl);
744
+ } catch {
745
+ return [];
746
+ }
747
+ const apiIdx = parsed.pathname.indexOf(API_V0_SEGMENT);
748
+ if (apiIdx === -1) {
749
+ return [];
750
+ }
751
+ if (SKIP_INVALIDATION_PATTERNS.some((p) => p.test(parsed.pathname))) {
752
+ return [];
753
+ }
754
+ const base = `${parsed.origin}${parsed.pathname.slice(0, apiIdx + API_V0_SEGMENT.length)}`;
755
+ const relPath = parsed.pathname.slice(apiIdx + API_V0_SEGMENT.length);
756
+ if (relPath === "") {
757
+ return [];
758
+ }
759
+ const prefixes = new Set;
760
+ for (const segments of ancestorSegments(relPath)) {
761
+ prefixes.add(`${base}${segments}`);
762
+ }
763
+ for (const extra of applyCrossEndpointRules(relPath, base, apiBaseUrl)) {
764
+ prefixes.add(extra);
765
+ }
766
+ return [...prefixes];
767
+ }
768
+ function* applyCrossEndpointRules(relPath, base, apiBaseUrl) {
769
+ for (const rule of CROSS_ENDPOINT_RULES) {
770
+ const match = relPath.match(rule.match);
771
+ if (!match) {
772
+ continue;
773
+ }
774
+ for (const extra of rule.extra?.(match) ?? []) {
775
+ yield `${base}${extra}`;
776
+ }
777
+ for (const absolute of rule.extraAbsolute?.(match, { apiBaseUrl }) ?? []) {
778
+ yield absolute;
779
+ }
780
+ }
781
+ }
782
+ function* ancestorSegments(relPath) {
783
+ const trimmed = relPath.endsWith("/") ? relPath.slice(0, -1) : relPath;
784
+ if (trimmed === "") {
785
+ return;
786
+ }
787
+ const parts = trimmed.split("/");
788
+ const floor = parts.length > 2 ? 2 : 1;
789
+ for (let i = parts.length;i >= floor; i--) {
790
+ yield `${parts.slice(0, i).join("/")}/`;
791
+ }
792
+ }
793
+ // src/api-client/response-cache/keys.ts
794
+ var encoder = new TextEncoder;
795
+ function normalizeUrl(method, url) {
796
+ const parsed = new URL(url);
797
+ const sortedParams = new URLSearchParams([...parsed.searchParams.entries()].sort(([a], [b]) => {
798
+ if (a < b) {
799
+ return -1;
800
+ }
801
+ if (a > b) {
802
+ return 1;
803
+ }
804
+ return 0;
805
+ }));
806
+ parsed.search = sortedParams.toString() ? `?${sortedParams.toString()}` : "";
807
+ return `${method.toUpperCase()}|${parsed.toString()}`;
808
+ }
809
+ async function buildCacheKey(method, url, identity) {
810
+ if (!identity) {
811
+ throw new TypeError("A cache key needs a non-empty identity");
812
+ }
813
+ const normalized = normalizeUrl(method, url);
814
+ const key = await crypto.subtle.importKey("raw", encoder.encode(identity), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
815
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(normalized));
816
+ return Array.from(new Uint8Array(signature), (byte) => byte.toString(16).padStart(2, "0")).join("");
817
+ }
818
+ // src/api-client/response-cache/memory-store.ts
819
+ var MIB = 1024 * 1024;
820
+ var ENTRY_OVERHEAD_BYTES = 1024;
821
+ function limit(name, value, fallback) {
822
+ if (value === undefined) {
823
+ return fallback;
824
+ }
825
+ if (!(Number.isFinite(value) && value > 0)) {
826
+ throw new RangeError(`${name} must be a positive number, got ${value}`);
827
+ }
828
+ return value;
829
+ }
830
+ function ownBytes(bytes) {
831
+ return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength ? bytes : new Uint8Array(bytes);
832
+ }
833
+ function createMemoryStore(options = {}) {
834
+ const maxBytes = limit("maxBytes", options.maxBytes, 24 * MIB);
835
+ const maxEntryBytes = limit("maxEntryBytes", options.maxEntryBytes, MIB);
836
+ const maxRawBytes = limit("maxRawBytes", options.maxRawBytes, 32 * MIB);
837
+ const slots = new Map;
838
+ let bytes = 0;
839
+ function remove(key) {
840
+ const slot = slots.get(key);
841
+ if (slot) {
842
+ slots.delete(key);
843
+ bytes -= slot.size;
844
+ }
845
+ }
846
+ return {
847
+ async get(key) {
848
+ const slot = slots.get(key);
849
+ if (!slot) {
850
+ return;
851
+ }
852
+ if (Date.now() >= slot.expiresAt) {
853
+ remove(key);
854
+ return;
855
+ }
856
+ slots.delete(key);
857
+ slots.set(key, slot);
858
+ return slot.entry;
859
+ },
860
+ async set(key, entry, ttlMs) {
861
+ remove(key);
862
+ const stored = entry.body.bytes.byteLength;
863
+ const size = stored + ENTRY_OVERHEAD_BYTES;
864
+ if (!(ttlMs > 0) || stored > maxEntryBytes || entry.body.rawSize > maxRawBytes || size > maxBytes) {
865
+ return;
866
+ }
867
+ const body = { ...entry.body, bytes: ownBytes(entry.body.bytes) };
868
+ slots.set(key, {
869
+ entry: { ...entry, body },
870
+ size,
871
+ expiresAt: Date.now() + ttlMs
872
+ });
873
+ bytes += size;
874
+ for (const oldest of slots.keys()) {
875
+ if (bytes <= maxBytes) {
876
+ break;
877
+ }
878
+ remove(oldest);
879
+ }
880
+ },
881
+ async deleteMatching(identity, urlPrefix) {
882
+ for (const [key, slot] of slots) {
883
+ if (slot.entry.identity === identity && slot.entry.url.startsWith(urlPrefix)) {
884
+ remove(key);
885
+ }
886
+ }
887
+ },
888
+ stats() {
889
+ return { entries: slots.size, bytes };
890
+ }
891
+ };
892
+ }
893
+ // src/api-client/response-cache/policy.ts
894
+ import CachePolicy from "http-cache-semantics";
895
+
896
+ // src/api-client/response-cache/tiers.ts
897
+ var FALLBACK_TTL_MS = {
898
+ stable: 5 * 60 * 1000,
899
+ volatile: 60 * 1000,
900
+ "no-cache": 0
901
+ };
902
+ var URL_TIER_PATTERNS = [
903
+ ["no-cache", [/\/(?:autofix|root-cause)\//]],
904
+ [
905
+ "volatile",
906
+ [
907
+ /\/issues\//,
908
+ /[?&]dataset=(?:logs|spans)/,
909
+ /\/trace-logs\//,
910
+ /\/trace\/[0-9a-f]{32}\//,
911
+ /\/projects\/[^/]+\/[^/]+\/events\/[^/?]+\/?(?:\?|$)/
912
+ ]
913
+ ]
914
+ ];
915
+ function classifyUrl(url) {
916
+ for (const [tier, patterns] of URL_TIER_PATTERNS) {
917
+ if (patterns.some((pattern) => pattern.test(url))) {
918
+ return tier;
919
+ }
920
+ }
921
+ return "stable";
922
+ }
923
+
924
+ // src/api-client/response-cache/policy.ts
925
+ var REVALIDATION_WINDOW_MS = 24 * 60 * 60 * 1000;
926
+ var POLICY_OPTIONS = {
927
+ shared: false,
928
+ cacheHeuristic: 0.1
929
+ };
930
+ var PRESERVED_HEADERS = ["link"];
931
+ var encoder2 = new TextEncoder;
932
+ function pickHeaders(headers) {
933
+ const result = {};
934
+ for (const name of PRESERVED_HEADERS) {
935
+ const value = headers.get(name);
936
+ if (value) {
937
+ result[name] = value;
938
+ }
939
+ }
940
+ return result;
941
+ }
942
+ async function policyRequestHeaders(headers) {
943
+ const result = {};
944
+ for (const [name, value] of Object.entries(headers)) {
945
+ result[name] = name.toLowerCase() === "authorization" && value ? await sha256Hex(value) : value;
946
+ }
947
+ return result;
948
+ }
949
+ async function sha256Hex(value) {
950
+ const digest = await crypto.subtle.digest("SHA-256", encoder2.encode(value));
951
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
952
+ }
953
+ function headersToObject(headers) {
954
+ return Object.fromEntries(headers.entries());
955
+ }
956
+ function hasServerCacheDirectives(policy) {
957
+ return Object.keys(policy.toObject().rescc).length > 0;
958
+ }
959
+ function isEntryFresh(policy, entry, request) {
960
+ if (policy.satisfiesWithoutRevalidation(request)) {
961
+ return true;
962
+ }
963
+ if (hasServerCacheDirectives(policy)) {
964
+ return false;
965
+ }
966
+ const fallbackTtl = FALLBACK_TTL_MS[classifyUrl(request.url)];
967
+ return Date.now() - entry.createdAt <= fallbackTtl;
968
+ }
969
+ function retentionMs(policy, tier) {
970
+ const freshMs = hasServerCacheDirectives(policy) ? policy.timeToLive() : FALLBACK_TTL_MS[tier];
971
+ return policy.responseHeaders().etag === undefined ? freshMs : freshMs + REVALIDATION_WINDOW_MS;
972
+ }
973
+ function buildResponseHeaders(policy, entry) {
974
+ const result = {};
975
+ for (const [name, value] of Object.entries(policy.responseHeaders())) {
976
+ if (value !== undefined) {
977
+ result[name] = Array.isArray(value) ? value.join(", ") : value;
978
+ }
979
+ }
980
+ for (const [name, value] of Object.entries(entry.headers)) {
981
+ if (!(name in result)) {
982
+ result[name] = value;
983
+ }
984
+ }
985
+ return result;
986
+ }
987
+ function responseBody(bytes) {
988
+ return bytes;
989
+ }
990
+ function staleLookup(options, entry, policy, request) {
991
+ const etag = policy.revalidationHeaders(request)["if-none-match"];
992
+ if (typeof etag !== "string") {
993
+ return;
994
+ }
995
+ const { store, codec, key, onRenewError } = options;
996
+ return {
997
+ kind: "stale",
998
+ conditionalHeaders: { "if-none-match": etag },
999
+ async revalidate(response) {
1000
+ if (response.status !== 304) {
1001
+ return;
1002
+ }
1003
+ const result = policy.revalidatedPolicy(request, {
1004
+ status: response.status,
1005
+ headers: headersToObject(response.headers)
1006
+ });
1007
+ if (result.modified || result.matches === false) {
1008
+ return;
1009
+ }
1010
+ const body = await codec.decode(entry.body);
1011
+ if (!body) {
1012
+ return;
1013
+ }
1014
+ const renewed = {
1015
+ ...entry,
1016
+ policy: result.policy.toObject(),
1017
+ createdAt: Date.now()
1018
+ };
1019
+ store.set(key, renewed, retentionMs(result.policy, entry.tier)).catch((error) => onRenewError?.(error));
1020
+ return new Response(responseBody(body), {
1021
+ status: entry.status,
1022
+ headers: buildResponseHeaders(result.policy, entry)
1023
+ });
1024
+ }
1025
+ };
1026
+ }
1027
+ async function lookupResponse(options) {
1028
+ const { store, codec, key, url, requestHeaders } = options;
1029
+ const entry = await store.get(key);
1030
+ if (!entry) {
1031
+ return;
1032
+ }
1033
+ const policy = CachePolicy.fromObject(entry.policy);
1034
+ const request = {
1035
+ url,
1036
+ method: "GET",
1037
+ headers: await policyRequestHeaders(requestHeaders)
1038
+ };
1039
+ if (!isEntryFresh(policy, entry, request)) {
1040
+ return staleLookup(options, entry, policy, request);
1041
+ }
1042
+ const body = await codec.decode(entry.body);
1043
+ if (!body) {
1044
+ return;
1045
+ }
1046
+ return {
1047
+ kind: "hit",
1048
+ response: new Response(responseBody(body), {
1049
+ status: entry.status,
1050
+ headers: buildResponseHeaders(policy, entry)
1051
+ }),
1052
+ createdAt: entry.createdAt,
1053
+ size: body.byteLength
1054
+ };
1055
+ }
1056
+ async function storeResponse(options) {
1057
+ const { store, codec, key, identity, url, requestHeaders, response } = options;
1058
+ const tier = classifyUrl(url);
1059
+ if (!response.ok || tier === "no-cache") {
1060
+ return 0;
1061
+ }
1062
+ const policy = new CachePolicy({
1063
+ url,
1064
+ method: "GET",
1065
+ headers: await policyRequestHeaders(requestHeaders)
1066
+ }, { status: response.status, headers: headersToObject(response.headers) }, POLICY_OPTIONS);
1067
+ if (!policy.storable()) {
1068
+ return 0;
1069
+ }
1070
+ const text = await response.text();
1071
+ JSON.parse(text);
1072
+ const createdAt = Date.now();
1073
+ const body = await codec.encode(encoder2.encode(text));
1074
+ await store.set(key, {
1075
+ url,
1076
+ identity,
1077
+ tier,
1078
+ status: response.status,
1079
+ headers: pickHeaders(response.headers),
1080
+ body,
1081
+ policy: policy.toObject(),
1082
+ createdAt
1083
+ }, retentionMs(policy, tier));
1084
+ return body.bytes.byteLength;
1085
+ }
1086
+ // src/api-client/time-params.ts
1087
+ function normalizeStatsPeriod(statsPeriod) {
1088
+ const normalized = statsPeriod?.trim();
1089
+ return normalized || undefined;
1090
+ }
1091
+ function defaultStatsPeriod({
1092
+ statsPeriod,
1093
+ start,
1094
+ end
1095
+ }) {
1096
+ const normalized = normalizeStatsPeriod(statsPeriod);
1097
+ return start || end ? normalized : normalized ?? "24h";
1098
+ }
1099
+ function parseStatsPeriod(statsPeriod) {
1100
+ const match = /^(\d+)([smhdw])$/.exec(statsPeriod);
1101
+ if (!match?.[2]) {
1102
+ throw new ApiValidationError("statsPeriod must use a supported relative time format, such as `24h`, `7d`, or `2w`.");
1103
+ }
1104
+ return {
1105
+ amount: Number(match[1]),
1106
+ unit: match[2]
1107
+ };
1108
+ }
1109
+ function applyTimeParams(queryParams, statsPeriod, start, end) {
1110
+ if (statsPeriod && (start || end)) {
1111
+ throw new ApiValidationError("Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.");
1112
+ }
1113
+ if (start && !end || !start && end) {
1114
+ throw new ApiValidationError("Both start and end parameters must be provided together for absolute time ranges.");
1115
+ }
1116
+ if (statsPeriod) {
1117
+ queryParams.set("statsPeriod", statsPeriod);
1118
+ } else if (start && end) {
1119
+ queryParams.set("start", start);
1120
+ queryParams.set("end", end);
1121
+ }
1122
+ }
1123
+ function applyStatsMixinTimeParams(queryParams, statsPeriod, start, end, resolutionSeconds) {
1124
+ if (statsPeriod && (start || end)) {
1125
+ throw new ApiValidationError("Cannot use both statsPeriod and start/end parameters. Use either statsPeriod for relative time or start/end for absolute time.");
1126
+ }
1127
+ if (start && !end || !start && end) {
1128
+ throw new ApiValidationError("Both start and end parameters must be provided together for absolute time ranges.");
1129
+ }
1130
+ let since;
1131
+ let until;
1132
+ if (start && end) {
1133
+ const startMs = Date.parse(start);
1134
+ const endMs = Date.parse(end);
1135
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs)) {
1136
+ throw new ApiValidationError("start and end parameters must be valid ISO 8601 date strings.");
1137
+ }
1138
+ since = Math.floor(startMs / 1000);
1139
+ until = Math.floor(endMs / 1000);
1140
+ } else {
1141
+ const period = statsPeriod ?? "24h";
1142
+ const { amount, unit } = parseStatsPeriod(period);
1143
+ let unitSeconds;
1144
+ switch (unit) {
1145
+ case "s":
1146
+ unitSeconds = 1;
1147
+ break;
1148
+ case "m":
1149
+ unitSeconds = 60;
1150
+ break;
1151
+ case "h":
1152
+ unitSeconds = 60 * 60;
1153
+ break;
1154
+ case "d":
1155
+ unitSeconds = 60 * 60 * 24;
1156
+ break;
1157
+ case "w":
1158
+ unitSeconds = 60 * 60 * 24 * 7;
1159
+ break;
1160
+ default:
1161
+ throw new ApiValidationError("statsPeriod must use a supported relative time format, such as `24h`, `7d`, or `2w`.");
1162
+ }
1163
+ until = Math.floor(Date.now() / 1000);
1164
+ since = until - amount * unitSeconds;
1165
+ }
1166
+ queryParams.set("since", String(since));
1167
+ queryParams.set("until", String(until));
1168
+ if (resolutionSeconds !== undefined) {
1169
+ queryParams.set("resolution", `${resolutionSeconds}s`);
1170
+ }
1171
+ }
1172
+ // src/api-client/transport.ts
1173
+ class ApiTimeoutError extends Error {
1174
+ timeoutMs;
1175
+ idempotent;
1176
+ constructor(timeoutMs, idempotent) {
1177
+ super(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
1178
+ this.name = "ApiTimeoutError";
1179
+ this.timeoutMs = timeoutMs;
1180
+ this.idempotent = idempotent;
1181
+ }
1182
+ }
1183
+ var MAX_REDIRECTS = 20;
1184
+ var ANONYMOUS = { token: "", identity: "" };
1185
+ var validatedSources = new WeakMap;
1186
+ function getValidatedResponseSource(response) {
1187
+ return validatedSources.get(response);
1188
+ }
1189
+ function extractInputUrl(input) {
1190
+ if (typeof input === "string") {
1191
+ return input;
1192
+ }
1193
+ if (input instanceof URL) {
1194
+ return input.href;
1195
+ }
1196
+ return input.url;
1197
+ }
1198
+ function extractUrlPath(input) {
1199
+ const raw = extractInputUrl(input);
1200
+ try {
1201
+ return new URL(raw).pathname;
1202
+ } catch {
1203
+ return raw;
1204
+ }
1205
+ }
1206
+
1207
+ class Rejection {
1208
+ error;
1209
+ constructor(error) {
1210
+ this.error = error;
1211
+ }
1212
+ }
1213
+ function createApiTransport(config) {
1214
+ const { credentials, retry, logger } = config;
1215
+ const cache = credentials && config.cache;
1216
+ const baseFetch = (input, init) => (config.fetch ?? globalThis.fetch)(input, init);
1217
+ function authorization(credential) {
1218
+ if (!credentials) {
1219
+ return;
1220
+ }
1221
+ if (credentials.authorization) {
1222
+ return credentials.authorization(credential);
1223
+ }
1224
+ const header = sentryBearerHeader(credential.token);
1225
+ if (header === null) {
1226
+ throw new Error("Malformed authentication token");
1227
+ }
1228
+ return header;
1229
+ }
1230
+ function lookupHeaders(credential) {
1231
+ try {
1232
+ const value = authorization(credential);
1233
+ return value ? { authorization: value } : {};
1234
+ } catch {
1235
+ return {};
1236
+ }
1237
+ }
1238
+ function timeoutFor(url) {
1239
+ return typeof config.timeoutMs === "function" ? config.timeoutMs(url) : config.timeoutMs;
1240
+ }
1241
+ function buildHeaders(input, init, credential) {
1242
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
1243
+ config.prepareHeaders?.(headers, { input, credential });
1244
+ setAuthorization(headers, authorization(credential));
1245
+ if (config.userAgent && !headers.has("User-Agent")) {
1246
+ headers.set("User-Agent", config.userAgent);
1247
+ }
1248
+ return headers;
1249
+ }
1250
+ function cancelBody(response) {
1251
+ if (response.body && !response.bodyUsed) {
1252
+ response.body.cancel().catch((error) => {
1253
+ logger?.debug("Failed to cancel unused response body", error);
1254
+ });
1255
+ }
1256
+ }
1257
+ async function fetchOnce(input, init, signal, timeoutMs, mode) {
1258
+ const controller = new AbortController;
1259
+ let timedOut = false;
1260
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => {
1261
+ timedOut = true;
1262
+ controller.abort();
1263
+ }, timeoutMs);
1264
+ const onAbort = () => controller.abort(signal?.reason);
1265
+ if (signal?.aborted) {
1266
+ onAbort();
1267
+ } else {
1268
+ signal?.addEventListener("abort", onAbort, { once: true });
1269
+ }
1270
+ try {
1271
+ const response = await baseFetch(input, {
1272
+ ...init,
1273
+ signal: controller.signal
1274
+ });
1275
+ if (shouldBuffer(response, mode)) {
1276
+ await drainBody(response.clone(), controller.signal);
1277
+ }
1278
+ return { response };
1279
+ } catch (error) {
1280
+ if (timedOut) {
1281
+ return { failed: error, timedOutAfterMs: timeoutMs };
1282
+ }
1283
+ throw error;
1284
+ } finally {
1285
+ clearTimeout(timer);
1286
+ signal?.removeEventListener("abort", onAbort);
1287
+ }
1288
+ }
1289
+ async function rejectRedirect(response, destination, credential) {
1290
+ cancelBody(response);
1291
+ throw new Rejection(config.origins?.reject(destination, credential) ?? new Error(`Refusing to follow a redirect to ${destination}`));
1292
+ }
1293
+ function trusts(url, credential, original) {
1294
+ return config.origins ? config.origins.trusts(url, credential) : url.origin === original.origin;
1295
+ }
1296
+ async function nextRedirect(response, request2, credential, visited, original) {
1297
+ const location = response.headers.get("location");
1298
+ if (!location) {
1299
+ return await rejectRedirect(response, "<unknown host>", credential);
1300
+ }
1301
+ if (visited.size > MAX_REDIRECTS) {
1302
+ return await rejectRedirect(response, "<too many redirects>", credential);
1303
+ }
1304
+ let next;
1305
+ try {
1306
+ next = new URL(location, request2.url);
1307
+ } catch (error) {
1308
+ logger?.debug("Rejected malformed redirect location", error instanceof Error ? error.name : "unknown error");
1309
+ return await rejectRedirect(response, "<invalid host>", credential);
1310
+ }
1311
+ if (!trusts(next, credential, original)) {
1312
+ return await rejectRedirect(response, next.origin, credential);
1313
+ }
1314
+ const key = redirectKey(next);
1315
+ if (visited.has(key)) {
1316
+ return await rejectRedirect(response, "<redirect loop>", credential);
1317
+ }
1318
+ visited.add(key);
1319
+ return next;
1320
+ }
1321
+ async function fetchValidated(input, init, headers, credential, timeoutMs, mode) {
1322
+ const original = new Request(input, init);
1323
+ const originalUrl = new URL(original.url);
1324
+ const visited = new Set([redirectKey(originalUrl)]);
1325
+ let request2 = original;
1326
+ let requestHeaders = new Headers(headers);
1327
+ for (;; ) {
1328
+ const result = await fetchOnce(request2.clone(), { headers: requestHeaders, redirect: "manual" }, init?.signal ?? original.signal, timeoutMs, mode);
1329
+ if (!("response" in result) || !isRedirect(result.response.status)) {
1330
+ const origin = httpOrigin(request2.url);
1331
+ if ("response" in result && origin) {
1332
+ validatedSources.set(result.response, {
1333
+ origin,
1334
+ identity: credential.identity
1335
+ });
1336
+ }
1337
+ return result;
1338
+ }
1339
+ const { response } = result;
1340
+ try {
1341
+ const next = await nextRedirect(response, request2, credential, visited, originalUrl);
1342
+ ({ request: request2, headers: requestHeaders } = await redirectRequest(request2, requestHeaders, next, response.status));
1343
+ } finally {
1344
+ cancelBody(response);
1345
+ }
1346
+ }
1347
+ }
1348
+ async function send(sendArgs) {
1349
+ const { input, init, method, url, path, options } = sendArgs;
1350
+ let credential = credentials?.prepare ? await credentials.prepare(sendArgs.credential) : sendArgs.credential;
1351
+ const headers = buildHeaders(input, init, credential);
1352
+ for (const [name, value] of Object.entries(sendArgs.conditionalHeaders ?? {})) {
1353
+ headers.set(name, value);
1354
+ }
1355
+ const nextInput = await replayableInput(input, init);
1356
+ const timeoutMs = timeoutFor(url);
1357
+ const idempotent = options.idempotent === true || (config.isIdempotent?.(method, url) ?? false);
1358
+ const mode = options.responseBody ?? "stream";
1359
+ const signal = init?.signal !== undefined ? init.signal : input instanceof Request ? input.signal : undefined;
1360
+ let refreshed = false;
1361
+ let waitedMs = 0;
1362
+ const retryDelay = (attempt, status, serverDelayMs) => nextRetryDelayMs({
1363
+ policy: retry,
1364
+ method,
1365
+ idempotent,
1366
+ status,
1367
+ serverDelayMs,
1368
+ attempt,
1369
+ waitedMs
1370
+ });
1371
+ async function onResponse(response, attempt) {
1372
+ if (response.status === 401) {
1373
+ const next = refreshed || !credentials?.refresh ? undefined : await credentials.refresh(credential);
1374
+ if (!next) {
1375
+ return { action: "done", response };
1376
+ }
1377
+ refreshed = true;
1378
+ credential = next;
1379
+ setAuthorization(headers, authorization(next));
1380
+ if (config.refreshedRequestHeader) {
1381
+ headers.set(config.refreshedRequestHeader, "1");
1382
+ }
1383
+ cancelBody(response);
1384
+ return { action: "replay" };
1385
+ }
1386
+ const serverDelayMs = parseRetryDelayMs(response.status, response.headers);
1387
+ const delayMs = retryDelay(attempt, response.status, serverDelayMs);
1388
+ if (delayMs !== undefined) {
1389
+ cancelBody(response);
1390
+ return { action: "retry", delayMs };
1391
+ }
1392
+ if (response.status === 429 && config.errors?.rateLimited) {
1393
+ const error = await config.errors.rateLimited(response, {
1394
+ path,
1395
+ retryAfterMs: serverDelayMs
1396
+ });
1397
+ if (error) {
1398
+ return { action: "throw", error };
1399
+ }
1400
+ }
1401
+ return { action: "done", response };
1402
+ }
1403
+ function onFailure({ failed, timedOutAfterMs }, attempt) {
1404
+ if (failed instanceof Rejection) {
1405
+ return { action: "throw", error: failed.error };
1406
+ }
1407
+ if (isAbort(failed, signal)) {
1408
+ return { action: "throw", error: failed };
1409
+ }
1410
+ const fatal = config.errors?.fatal?.(failed);
1411
+ if (fatal) {
1412
+ return { action: "throw", error: fatal };
1413
+ }
1414
+ const delayMs = retryDelay(attempt, undefined, undefined);
1415
+ if (delayMs !== undefined) {
1416
+ return { action: "retry", delayMs };
1417
+ }
1418
+ if (timedOutAfterMs === undefined) {
1419
+ return { action: "throw", error: failed };
1420
+ }
1421
+ const replayable = isIdempotentRequest(method, idempotent);
1422
+ return {
1423
+ action: "throw",
1424
+ error: config.errors?.timeout?.({
1425
+ timeoutMs: timedOutAfterMs,
1426
+ idempotent: replayable
1427
+ }) ?? new ApiTimeoutError(timedOutAfterMs, replayable)
1428
+ };
1429
+ }
1430
+ for (let attempt = 0;; ) {
1431
+ const { input: attemptInput, init: attemptInit } = nextInput();
1432
+ let outcome;
1433
+ try {
1434
+ outcome = options.validateRedirects ? await fetchValidated(attemptInput, attemptInit, headers, credential, timeoutMs, mode) : await fetchOnce(attemptInput, { ...attemptInit, headers }, signal, timeoutMs, mode);
1435
+ } catch (error) {
1436
+ outcome = { failed: error };
1437
+ }
1438
+ const result = "response" in outcome ? await onResponse(outcome.response, attempt) : onFailure(outcome, attempt);
1439
+ if (result.action === "done") {
1440
+ storeInCache(method, url, sendArgs.credential.identity, headers, result.response);
1441
+ await invalidateAfterWrite(method, url, sendArgs.credential.identity, result.response);
1442
+ return result.response;
1443
+ }
1444
+ if (result.action === "throw") {
1445
+ throw result.error;
1446
+ }
1447
+ if (result.action === "replay") {
1448
+ logger?.debug(`${method} ${path} → replay with a refreshed credential`);
1449
+ continue;
1450
+ }
1451
+ logger?.debug(`${method} ${path} → retry ${attempt + 1}/${retry.maxRetries} after ${Math.round(result.delayMs)}ms`);
1452
+ await sleep(result.delayMs, signal);
1453
+ waitedMs += result.delayMs;
1454
+ attempt++;
1455
+ }
1456
+ }
1457
+ function storeInCache(method, url, identity, headers, response) {
1458
+ if (!cache || method !== "GET" || !response.ok) {
1459
+ return;
1460
+ }
1461
+ cache.store({
1462
+ url,
1463
+ requestHeaders: { authorization: headers.get("Authorization") ?? "" },
1464
+ identity
1465
+ }, response.clone()).catch((error) => {
1466
+ logger?.debug("Response cache write failed", error);
1467
+ });
1468
+ }
1469
+ async function invalidateAfterWrite(method, url, identity, response) {
1470
+ if (!cache || method === "GET" || !response.ok) {
1471
+ return;
1472
+ }
1473
+ try {
1474
+ await cache.invalidate(url, identity);
1475
+ } catch (error) {
1476
+ logger?.debug("Post-mutation cache invalidation failed", error);
1477
+ }
1478
+ }
1479
+ async function request(input, init, options = {}) {
1480
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
1481
+ const url = extractInputUrl(input);
1482
+ const path = extractUrlPath(input);
1483
+ const run = async () => {
1484
+ const startTime = performance.now();
1485
+ const elapsed = () => (performance.now() - startTime).toFixed(0);
1486
+ const credential = credentials ? await credentials.current() : ANONYMOUS;
1487
+ const lookup = cache && method === "GET" && !options.validateRedirects && !config.fresh?.() ? await cache.lookup({
1488
+ url,
1489
+ requestHeaders: lookupHeaders(credential),
1490
+ identity: credential.identity
1491
+ }) : undefined;
1492
+ if (lookup?.kind === "hit") {
1493
+ logger?.debug(`${method} ${path} → ${lookup.response.status} (cache hit, ${elapsed()}ms)`);
1494
+ return lookup.response;
1495
+ }
1496
+ const sendArgs = { input, init, method, url, path, credential, options };
1497
+ let response = await send({
1498
+ ...sendArgs,
1499
+ conditionalHeaders: lookup?.conditionalHeaders
1500
+ });
1501
+ if (lookup?.kind === "stale" && response.status === 304) {
1502
+ const revalidated = await lookup.revalidate(response);
1503
+ if (revalidated) {
1504
+ logger?.debug(`${method} ${path} → 304 (revalidated cache entry, ${elapsed()}ms)`);
1505
+ return revalidated;
1506
+ }
1507
+ response = await send(sendArgs);
1508
+ }
1509
+ logger?.debug(`${method} ${path} → ${response.status} (${elapsed()}ms)`);
1510
+ return response;
1511
+ };
1512
+ return config.span ? await config.span({ method, path }, run) : await run();
1513
+ }
1514
+ return { request };
1515
+ }
1516
+ async function drainBody(response, signal) {
1517
+ const drained = response.body?.pipeTo(new WritableStream);
1518
+ if (!drained) {
1519
+ return;
1520
+ }
1521
+ let onAbort;
1522
+ const aborted = new Promise((_, reject) => {
1523
+ onAbort = () => reject(signal.reason);
1524
+ if (signal.aborted) {
1525
+ onAbort();
1526
+ } else {
1527
+ signal.addEventListener("abort", onAbort, { once: true });
1528
+ }
1529
+ });
1530
+ try {
1531
+ await Promise.race([drained, aborted]);
1532
+ } finally {
1533
+ if (onAbort) {
1534
+ signal.removeEventListener("abort", onAbort);
1535
+ }
1536
+ drained.catch(() => {});
1537
+ }
1538
+ }
1539
+ function isAbort(error, signal) {
1540
+ return Boolean(signal?.aborted) && (error === signal?.reason || error instanceof DOMException && error.name === "AbortError");
1541
+ }
1542
+ function shouldBuffer(response, mode) {
1543
+ const contentType = response.headers.get("Content-Type");
1544
+ if (contentType?.split(";")[0]?.trim().toLowerCase() === "text/event-stream") {
1545
+ return false;
1546
+ }
1547
+ return response.ok && (mode === "buffer" || mode === "auto" && !!contentType);
1548
+ }
1549
+ async function replayableInput(input, init) {
1550
+ if (input instanceof Request) {
1551
+ return () => ({ input: input.clone(), init });
1552
+ }
1553
+ const body = init?.body;
1554
+ if (body instanceof ReadableStream) {
1555
+ const snapshot = await new Response(body).arrayBuffer();
1556
+ return () => ({ input, init: { ...init, body: snapshot } });
1557
+ }
1558
+ return () => ({ input, init });
1559
+ }
1560
+ function setAuthorization(headers, value) {
1561
+ if (value !== undefined) {
1562
+ headers.set("Authorization", value);
1563
+ }
1564
+ }
1565
+ function httpOrigin(raw) {
1566
+ const url = new URL(raw);
1567
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password ? url.origin : undefined;
1568
+ }
1569
+ function isRedirect(status) {
1570
+ return [301, 302, 303, 307, 308].includes(status);
1571
+ }
1572
+ function redirectKey(url) {
1573
+ const key = new URL(url);
1574
+ key.hash = "";
1575
+ return key.href;
1576
+ }
1577
+ async function redirectRequest(request, requestHeaders, next, status) {
1578
+ const current = request.method.toUpperCase();
1579
+ const method = status === 303 && current !== "HEAD" || (status === 301 || status === 302) && current === "POST" ? "GET" : current;
1580
+ const headers = new Headers(requestHeaders);
1581
+ if (method !== current) {
1582
+ headers.delete("content-length");
1583
+ headers.delete("content-type");
1584
+ headers.delete("transfer-encoding");
1585
+ }
1586
+ const body = method === current && request.body ? await request.clone().arrayBuffer() : undefined;
1587
+ return { request: new Request(next, { method, headers, body }), headers };
1588
+ }
1589
+ export {
1590
+ withQuery,
1591
+ validateResponse,
1592
+ trimAuthToken,
1593
+ storeResponse,
1594
+ sleep,
1595
+ sentryBearerHeader,
1596
+ selectIdempotentWrites,
1597
+ readJsonResponse,
1598
+ readErrorResponse,
1599
+ parseStatsPeriod,
1600
+ parseRetryDelayMs,
1601
+ paginate,
1602
+ normalizeUrl,
1603
+ normalizeStatsPeriod,
1604
+ normalizeAuthToken,
1605
+ nextRetryDelayMs,
1606
+ nextPageCursor,
1607
+ lookupResponse,
1608
+ listPage,
1609
+ isIdempotentRequest,
1610
+ isApiAuthenticationErrorDeep,
1611
+ getValidatedResponseSource,
1612
+ fetchAllPages,
1613
+ extractUrlPath,
1614
+ extractInputUrl,
1615
+ encodeApiPathSegment,
1616
+ defaultStatsPeriod,
1617
+ decodePageCursor,
1618
+ createMemoryStore,
1619
+ createIdempotentOperationMatcher,
1620
+ createApiTransport,
1621
+ createApiError,
1622
+ createApiClientCore,
1623
+ consumeCursorPage,
1624
+ computeInvalidationPrefixes,
1625
+ classifyUrl,
1626
+ buildSentryApiUrl,
1627
+ buildCacheKey,
1628
+ applyTimeParams,
1629
+ applyStatsMixinTimeParams,
1630
+ apiPath,
1631
+ UserInputError,
1632
+ REVALIDATION_WINDOW_MS,
1633
+ MAX_LIST_PAGES,
1634
+ FALLBACK_TTL_MS,
1635
+ ConfigurationError,
1636
+ ApiValidationError,
1637
+ ApiTimeoutError,
1638
+ ApiServerError,
1639
+ ApiResponseValidationError,
1640
+ ApiRateLimitError,
1641
+ ApiPermissionError,
1642
+ ApiNotFoundError,
1643
+ ApiError,
1644
+ ApiClientError,
1645
+ ApiAuthenticationError,
1646
+ API_MAX_PER_PAGE
1647
+ };