@rawdash/connector-sentry 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,7 @@ A Sentry auth token is required. Use an organization-level Internal Integration
42
42
  - Events are sampled: at most eventsPerIssueCap recent events per issue per sync (Sentry caps a single events page at 100), so this is a representative sample, not a full audit trail.
43
43
  - **`sentry_release`** _(entity)_ - Releases with their versions, associated project slugs, and creation/release/last-event timestamps.
44
44
  - Endpoint: `GET /api/0/organizations/{organization}/releases/`
45
- - **`sentry_errors_per_hour`** _(metric)_ - Hourly count of error events, broken down by project, over the configured lookback window.
45
+ - **`sentry_errors_per_hour`** _(metric)_ - Hourly count of accepted (stored) error events, broken down by project, over the configured lookback window.
46
46
  - Endpoint: `GET /api/0/organizations/{organization}/stats_v2/`
47
47
  - Unit: errors
48
48
  - Granularity: 1h
@@ -101,7 +101,7 @@ Sentry returns X-Sentry-Rate-Limit-Remaining / X-Sentry-Rate-Limit-Reset headers
101
101
 
102
102
  ## Links
103
103
 
104
- - [Rawdash docs](https://rawdash.dev/docs/connectors/)
104
+ - [Rawdash docs](https://rawdash.dev/docs/connectors)
105
105
  - [Sentry API docs](https://docs.sentry.io/api/)
106
106
  - [GitHub](https://github.com/rawdash/rawdash)
107
107
 
package/dist/index.d.ts CHANGED
@@ -173,7 +173,7 @@ declare const sentryResources: {
173
173
  };
174
174
  readonly sentry_errors_per_hour: {
175
175
  readonly shape: "metric";
176
- readonly description: "Hourly count of error events, broken down by project, over the configured lookback window.";
176
+ readonly description: "Hourly count of accepted (stored) error events, broken down by project, over the configured lookback window.";
177
177
  readonly endpoint: "GET /api/0/organizations/{organization}/stats_v2/";
178
178
  readonly unit: "errors";
179
179
  readonly granularity: "1h";
@@ -339,7 +339,7 @@ declare class SentryConnector extends BaseConnector<SentrySettings, SentryCreden
339
339
  };
340
340
  readonly sentry_errors_per_hour: {
341
341
  readonly shape: "metric";
342
- readonly description: "Hourly count of error events, broken down by project, over the configured lookback window.";
342
+ readonly description: "Hourly count of accepted (stored) error events, broken down by project, over the configured lookback window.";
343
343
  readonly endpoint: "GET /api/0/organizations/{organization}/stats_v2/";
344
344
  readonly unit: "errors";
345
345
  readonly granularity: "1h";
package/dist/index.js CHANGED
@@ -382,7 +382,7 @@ var sentryResources = defineResources({
382
382
  },
383
383
  sentry_errors_per_hour: {
384
384
  shape: "metric",
385
- description: "Hourly count of error events, broken down by project, over the configured lookback window.",
385
+ description: "Hourly count of accepted (stored) error events, broken down by project, over the configured lookback window.",
386
386
  endpoint: "GET /api/0/organizations/{organization}/stats_v2/",
387
387
  unit: "errors",
388
388
  granularity: "1h",
@@ -526,6 +526,7 @@ var SentryConnector = class _SentryConnector extends BaseConnector {
526
526
  "per_page",
527
527
  String(clampPageSize(options.pageSize, RELEASES_PAGE_SIZE))
528
528
  );
529
+ u.searchParams.set("sort", "date");
529
530
  for (const project of this.settings.projects ?? []) {
530
531
  u.searchParams.append("project", project);
531
532
  }
@@ -538,11 +539,17 @@ var SentryConnector = class _SentryConnector extends BaseConnector {
538
539
  );
539
540
  u.searchParams.set("field", "sum(quantity)");
540
541
  u.searchParams.set("category", "error");
542
+ u.searchParams.set("outcome", "accepted");
541
543
  u.searchParams.set("interval", "1h");
542
544
  u.searchParams.set("statsPeriod", `${lookback}h`);
543
545
  u.searchParams.append("groupBy", "project");
544
- for (const project of this.settings.projects ?? []) {
545
- u.searchParams.append("project", project);
546
+ const projects = this.settings.projects ?? [];
547
+ if (projects.length > 0) {
548
+ for (const project of projects) {
549
+ u.searchParams.append("project", project);
550
+ }
551
+ } else {
552
+ u.searchParams.append("project", "-1");
546
553
  }
547
554
  return u.toString();
548
555
  }
@@ -591,11 +598,11 @@ var SentryConnector = class _SentryConnector extends BaseConnector {
591
598
  const releases = res.body;
592
599
  const cutoff = options.since ? new Date(options.since).getTime() : null;
593
600
  const filtered = cutoff !== null ? releases.filter((r) => {
594
- const ts = new Date(r.dateReleased ?? r.dateCreated).getTime();
601
+ const ts = new Date(r.dateCreated).getTime();
595
602
  return Number.isFinite(ts) ? ts >= cutoff : true;
596
603
  }) : releases;
597
604
  const lastRelease = releases.at(-1);
598
- const lastTs = lastRelease ? new Date(lastRelease.dateReleased ?? lastRelease.dateCreated).getTime() : null;
605
+ const lastTs = lastRelease ? new Date(lastRelease.dateCreated).getTime() : null;
599
606
  const cutoffReached = cutoff !== null && lastTs !== null && Number.isFinite(lastTs) && lastTs < cutoff;
600
607
  const next = !cutoffReached && nextLink && nextLink.hasResults ? this.sanitizePageUrl("releases", nextLink.url) : null;
601
608
  return { items: filtered, next };
@@ -696,23 +703,36 @@ var SentryConnector = class _SentryConnector extends BaseConnector {
696
703
  }
697
704
  async writeErrorStats(storage, stats) {
698
705
  const samples = [];
699
- const intervals = stats.intervals ?? [];
706
+ let intervals = stats.intervals ?? [];
707
+ if (intervals.length === 0 && stats.start) {
708
+ const seriesLen = stats.groups.reduce((max, group) => {
709
+ const len = group.series?.["sum(quantity)"]?.length ?? 0;
710
+ return Math.max(max, len);
711
+ }, 0);
712
+ const startMs = parseEpoch(stats.start, "iso");
713
+ if (seriesLen > 0 && startMs !== null) {
714
+ intervals = Array.from(
715
+ { length: seriesLen },
716
+ (_, i) => new Date(startMs + i * 36e5).toISOString()
717
+ );
718
+ }
719
+ }
700
720
  for (const group of stats.groups) {
701
721
  const project = group.by["project"];
702
722
  const projectKey = project !== void 0 ? String(project) : "unknown";
703
723
  const series = group.series?.["sum(quantity)"] ?? [];
704
- if (series.length === 0) {
705
- continue;
706
- }
707
724
  for (let i = 0; i < intervals.length; i++) {
708
725
  const intervalIso = intervals[i];
709
- const rawValue = series[i];
710
- if (intervalIso === void 0 || rawValue === void 0) {
726
+ if (intervalIso === void 0) {
711
727
  continue;
712
728
  }
713
729
  const ts = parseEpoch(intervalIso, "iso");
714
- const value = Number(rawValue);
715
- if (ts === null || !Number.isFinite(value)) {
730
+ if (ts === null) {
731
+ continue;
732
+ }
733
+ const rawValue = series[i];
734
+ const value = rawValue === void 0 ? 0 : Number(rawValue);
735
+ if (!Number.isFinite(value)) {
716
736
  continue;
717
737
  }
718
738
  samples.push({
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/sentry.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n mapWithConcurrency,\n parseEpoch,\n sanitizeAllowedUrl,\n standardRateLimitPolicy,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n authToken: z.object({ $secret: z.string() }).meta({\n label: 'Auth Token',\n description:\n 'Sentry Internal Integration token or User Auth Token. Create one at Sentry → Settings → Auth Tokens (or for an org, Settings → Custom Integrations → New Internal Integration).',\n placeholder: 'sntrys_...',\n secret: true,\n }),\n organization: z.string().min(1).meta({\n label: 'Organization slug',\n description: \"Your Sentry organization's slug, as it appears in the URL.\",\n placeholder: 'acme',\n }),\n projects: z.array(z.string().min(1)).nonempty().optional().meta({\n label: 'Projects (optional)',\n description:\n 'Restrict the sync to specific Sentry project slugs (or numeric IDs). Omit to sync every project the token can see.',\n }),\n resources: z\n .array(z.enum(['issues', 'issue_events', 'releases', 'errors_per_hour']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n \"Which Sentry resources to sync. Omit to sync all of them. 'issue_events' depends on 'issues' being fetched - enabling it without 'issues' still runs the issues query, but skips writing issue entities.\",\n }),\n eventsPerIssueCap: z.number().int().positive().max(100).optional().meta({\n label: 'Events per issue cap',\n description:\n 'Maximum number of recent events (occurrences) to sample per issue on each sync. Defaults to 100 (the max page size Sentry allows for the issue events endpoint).',\n placeholder: '100',\n }),\n statsLookbackHours: z.number().int().positive().max(168).optional().meta({\n label: 'Stats lookback (hours)',\n description:\n 'How many hours of hourly error-rate data to refresh on each sync. Defaults to 24.',\n placeholder: '24',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Sentry',\n category: 'engineering',\n brandColor: '#362D59',\n tagline:\n 'Sync issues, issue events, releases, and hourly error rates from a Sentry organization.',\n vendor: {\n name: 'Sentry',\n domain: 'sentry.io',\n apiDocs: 'https://docs.sentry.io/api/',\n website: 'https://sentry.io',\n },\n auth: {\n summary:\n 'A Sentry auth token is required. Use an organization-level Internal Integration token or a User Auth Token with read access to issues, events, and releases.',\n setup: [\n 'Open Sentry → Settings → Custom Integrations → New Internal Integration (or Settings → Auth Tokens for a personal token).',\n 'Grant read access to Issues & Events and Releases.',\n 'Copy the generated token and store it as a secret, referencing it from the connector config as `authToken: secret(\"SENTRY_AUTH_TOKEN\")`.',\n 'Set the `organization` slug as it appears in your Sentry URL.',\n ],\n },\n rateLimit:\n 'Sentry returns X-Sentry-Rate-Limit-Remaining / X-Sentry-Rate-Limit-Reset headers (reset in seconds); list pagination uses the Link header (page size 100).',\n limitations: [\n 'Performance / trace data is out of scope (high cost, low signal for dashboards).',\n 'Self-hosted Sentry on custom hosts is out of scope (pagination URLs are pinned to sentry.io).',\n ],\n});\n\nexport type SentryResource =\n | 'issues'\n | 'issue_events'\n | 'releases'\n | 'errors_per_hour';\n\nexport interface SentrySettings {\n organization: string;\n projects?: readonly string[];\n resources?: readonly SentryResource[];\n eventsPerIssueCap?: number;\n statsLookbackHours?: number;\n}\n\nconst sentryCredentials = {\n authToken: {\n description: 'Sentry auth token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype SentryCredentials = typeof sentryCredentials;\n\nconst sentryRateLimit = standardRateLimitPolicy({\n remainingHeader: 'x-sentry-rate-limit-remaining',\n resetHeader: 'x-sentry-rate-limit-reset',\n resetUnit: 's',\n});\n\nconst PHASE_ORDER = ['issues', 'releases', 'error_stats'] as const;\n\ntype SentryPhase = (typeof PHASE_ORDER)[number];\n\ntype SentrySyncCursor = ChunkedSyncCursor<SentryPhase, string>;\n\nconst isSentrySyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\ninterface SentryProjectRef {\n id?: string | number;\n slug: string;\n name?: string;\n platform?: string;\n}\n\ninterface SentryIssue {\n id: string;\n shortId: string;\n title: string;\n level: string;\n status: string;\n firstSeen: string;\n lastSeen: string;\n count: string | number;\n userCount: number;\n project: SentryProjectRef;\n}\n\ninterface SentryIssueEvent {\n id?: string;\n eventID?: string;\n dateCreated: string;\n message?: string | null;\n platform?: string | null;\n groupID?: string;\n environment?: string | null;\n}\n\ninterface SentryRelease {\n version: string;\n dateCreated: string;\n dateReleased?: string | null;\n lastEvent?: string | null;\n projects: SentryProjectRef[];\n}\n\ninterface SentryStatsResponse {\n intervals?: string[];\n groups: Array<{\n by: Record<string, string | number>;\n totals?: Record<string, number>;\n series?: Record<string, number[]>;\n }>;\n start?: string;\n end?: string;\n}\n\ninterface IssuesPageItem {\n issues: SentryIssue[];\n eventsByIssue: Map<string, SentryIssueEvent[]>;\n}\n\ninterface SentryLink {\n url: string;\n hasResults: boolean;\n}\n\nfunction parseSentryLink(\n header: string | null,\n rel: string,\n): SentryLink | null {\n if (!header) {\n return null;\n }\n for (const part of header.split(',')) {\n const m = part.match(/<([^>]+)>\\s*;\\s*(.+)$/);\n if (!m) {\n continue;\n }\n const url = m[1]!;\n const attrs = m[2]!;\n const relMatch = attrs.match(/rel=\"([^\"]+)\"/);\n if (!relMatch || relMatch[1] !== rel) {\n continue;\n }\n const resultsMatch = attrs.match(/results=\"([^\"]+)\"/);\n const hasResults = resultsMatch ? resultsMatch[1] === 'true' : true;\n return { url, hasResults };\n }\n return null;\n}\n\nconst SENTRY_API_HOST = 'sentry.io';\nconst SENTRY_API_BASE = `https://${SENTRY_API_HOST}/api/0`;\nconst DEFAULT_EVENTS_PER_ISSUE = 100;\nconst DEFAULT_STATS_LOOKBACK_HOURS = 24;\nconst MAX_PAGE_SIZE = 100;\nconst ISSUES_PAGE_SIZE = 100;\nconst RELEASES_PAGE_SIZE = 100;\nconst EVENT_FETCH_CONCURRENCY = 5;\nconst CHUNK_BUDGET_MS = 25_000;\n\nfunction clampPageSize(\n requested: number | undefined,\n fallback: number,\n): number {\n const n = requested ?? fallback;\n if (!Number.isFinite(n) || n < 1) {\n return 1;\n }\n return Math.min(Math.floor(n), MAX_PAGE_SIZE);\n}\n\nconst idString = z.string().min(1);\n\nconst issueResponseSchema = z.array(\n z.object({\n id: idString,\n shortId: z.string(),\n title: z.string(),\n level: z.enum(['debug', 'info', 'warning', 'error', 'fatal']),\n status: z.enum(['resolved', 'unresolved', 'ignored']),\n firstSeen: z.iso.datetime(),\n lastSeen: z.iso.datetime(),\n count: z.union([z.string().regex(/^\\d+$/), z.number().int().nonnegative()]),\n userCount: z.number().int().nonnegative(),\n project: z.object({\n slug: z.string().min(1),\n id: z.union([idString, z.number()]).optional(),\n name: z.string().optional(),\n platform: z.string().nullable().optional(),\n }),\n annotations: z.unknown().optional(),\n assignedTo: z.unknown().nullable().optional(),\n culprit: z.string().nullable().optional(),\n filtered: z.unknown().nullable().optional(),\n hasSeen: z.boolean().optional(),\n isBookmarked: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n isSubscribed: z.boolean().optional(),\n isUnhandled: z.boolean().optional(),\n issueCategory: z.string().optional(),\n issueType: z.string().optional(),\n lifetime: z.unknown().optional(),\n logger: z.string().nullable().optional(),\n metadata: z.unknown().optional(),\n numComments: z.number().int().nonnegative().optional(),\n permalink: z.string().optional(),\n platform: z.string().nullable().optional(),\n priority: z.string().optional(),\n priorityLockedAt: z.iso.datetime().nullable().optional(),\n seerAutofixLastTriggered: z.iso.datetime().nullable().optional(),\n seerExplorerAutofixLastTriggered: z.iso.datetime().nullable().optional(),\n seerFixabilityScore: z.number().nullable().optional(),\n shareId: z.string().nullable().optional(),\n stats: z.unknown().optional(),\n statusDetails: z.unknown().optional(),\n subscriptionDetails: z.unknown().nullable().optional(),\n substatus: z.string().nullable().optional(),\n type: z.string().optional(),\n }),\n);\n\nconst issueEventResponseSchema = z.array(\n z.object({\n id: z.string().optional(),\n eventID: z.string().optional(),\n dateCreated: z.iso.datetime(),\n message: z.string().nullable().optional(),\n platform: z.string().nullable().optional(),\n groupID: z.string().optional(),\n environment: z.string().nullable().optional(),\n crashFile: z.unknown().nullable().optional(),\n culprit: z.string().optional(),\n 'event.type': z.string().optional(),\n location: z.string().optional(),\n metadata: z.unknown().optional(),\n projectID: z.string().optional(),\n tags: z.unknown().optional(),\n title: z.string().optional(),\n user: z.unknown().optional(),\n }),\n);\n\nconst releaseResponseSchema = z.array(\n z.object({\n version: idString,\n dateCreated: z.iso.datetime(),\n dateReleased: z.iso.datetime().nullable().optional(),\n lastEvent: z.iso.datetime().nullable().optional(),\n projects: z.array(\n z.object({\n slug: z.string().min(1),\n hasHealthData: z.unknown().optional(),\n id: z.unknown().optional(),\n name: z.unknown().optional(),\n newGroups: z.unknown().optional(),\n platform: z.unknown().optional(),\n platforms: z.unknown().optional(),\n }),\n ),\n authors: z.unknown().optional(),\n commitCount: z.unknown().optional(),\n currentProjectMeta: z.unknown().optional(),\n data: z.unknown().optional(),\n deployCount: z.unknown().optional(),\n firstEvent: z.unknown().nullable().optional(),\n id: z.unknown().optional(),\n lastCommit: z.unknown().optional(),\n lastDeploy: z.unknown().optional(),\n newGroups: z.unknown().optional(),\n owner: z.unknown().optional(),\n ref: z.unknown().nullable().optional(),\n shortVersion: z.unknown().optional(),\n status: z.unknown().optional(),\n url: z.unknown().nullable().optional(),\n userAgent: z.unknown().nullable().optional(),\n versionInfo: z.unknown().optional(),\n }),\n);\n\nconst errorStatsResponseSchema = z.object({\n intervals: z.array(z.iso.datetime()).optional(),\n groups: z.array(\n z.object({\n by: z.record(z.string(), z.union([z.string(), z.number()])),\n totals: z.record(z.string(), z.number()).optional(),\n series: z.record(z.string(), z.array(z.number())).optional(),\n }),\n ),\n start: z.string().optional(),\n end: z.string().optional(),\n});\n\nexport const sentryResources = defineResources({\n sentry_issue: {\n shape: 'entity',\n description:\n 'Sentry issues (error groups) with level, status, occurrence count, affected user count, and first/last seen timestamps.',\n endpoint: 'GET /api/0/organizations/{organization}/issues/',\n filterable: [\n {\n field: 'status',\n ops: ['eq'],\n values: ['resolved', 'unresolved', 'ignored'],\n },\n {\n field: 'level',\n ops: ['eq'],\n values: ['fatal', 'error', 'warning', 'info', 'debug', 'sample'],\n },\n ],\n responses: { issues: issueResponseSchema },\n },\n sentry_issue_event: {\n shape: 'event',\n description:\n 'Individual event occurrences sampled per issue, with platform, environment, level, and message.',\n endpoint: 'GET /api/0/issues/{issueId}/events/',\n notes:\n 'Events are sampled: at most eventsPerIssueCap recent events per issue per sync (Sentry caps a single events page at 100), so this is a representative sample, not a full audit trail.',\n filterable: [],\n responses: { issue_events: issueEventResponseSchema },\n },\n sentry_release: {\n shape: 'entity',\n description:\n 'Releases with their versions, associated project slugs, and creation/release/last-event timestamps.',\n endpoint: 'GET /api/0/organizations/{organization}/releases/',\n filterable: [],\n responses: { releases: releaseResponseSchema },\n },\n sentry_errors_per_hour: {\n shape: 'metric',\n description:\n 'Hourly count of error events, broken down by project, over the configured lookback window.',\n endpoint: 'GET /api/0/organizations/{organization}/stats_v2/',\n unit: 'errors',\n granularity: '1h',\n dimensions: [\n { name: 'project', description: 'Sentry project slug or id.' },\n ],\n responses: { error_stats: errorStatsResponseSchema },\n },\n});\n\nexport const id = 'sentry';\n\nfunction pushableIssueQueryTerms(filter: FilterClause[] | undefined): string[] {\n if (!filter) {\n return [];\n }\n const terms: string[] = [];\n for (const clause of filter) {\n if (!('field' in clause) || clause.op !== 'eq') {\n continue;\n }\n if (clause.field === 'status' && typeof clause.value === 'string') {\n terms.push(`is:${clause.value}`);\n } else if (clause.field === 'level' && typeof clause.value === 'string') {\n terms.push(`level:${clause.value}`);\n }\n }\n return terms;\n}\n\nexport class SentryConnector extends BaseConnector<\n SentrySettings,\n SentryCredentials\n> {\n static readonly id = id;\n\n static readonly resources = sentryResources;\n\n static readonly schemas = schemasFromResources(sentryResources);\n\n static create(input: unknown, ctx?: ConnectorContext): SentryConnector {\n const parsed = configFields.parse(input);\n return new SentryConnector(\n {\n organization: parsed.organization,\n projects: parsed.projects,\n resources: parsed.resources,\n eventsPerIssueCap: parsed.eventsPerIssueCap,\n statsLookbackHours: parsed.statsLookbackHours,\n },\n { authToken: parsed.authToken },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = sentryCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.authToken}`,\n 'User-Agent': connectorUserAgent('sentry'),\n };\n }\n\n private fetch<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n rateLimit: sentryRateLimit,\n });\n }\n\n private activePhases(): SentryPhase[] {\n return selectActivePhases<SentryResource, SentryPhase>(\n (r) => {\n switch (r) {\n case 'issues':\n case 'issue_events':\n return 'issues';\n case 'releases':\n return 'releases';\n case 'errors_per_hour':\n return 'error_stats';\n }\n },\n PHASE_ORDER,\n this.settings.resources,\n );\n }\n\n private allowedPagePath(phase: SentryPhase): string | null {\n const org = this.settings.organization;\n switch (phase) {\n case 'issues':\n return `/api/0/organizations/${org}/issues/`;\n case 'releases':\n return `/api/0/organizations/${org}/releases/`;\n case 'error_stats':\n return null;\n }\n }\n\n private sanitizePageUrl(\n phase: SentryPhase,\n pageUrl: string | null,\n ): string | null {\n const allowedPath = this.allowedPagePath(phase);\n if (allowedPath === null) {\n return null;\n }\n return sanitizeAllowedUrl({\n url: pageUrl,\n host: SENTRY_API_HOST,\n pathname: allowedPath,\n });\n }\n\n private resolveCursor(cursor: unknown): SentrySyncCursor | undefined {\n if (!isSentrySyncCursor(cursor)) {\n return undefined;\n }\n return {\n phase: cursor.phase,\n page: this.sanitizePageUrl(cursor.phase, cursor.page),\n };\n }\n\n private buildInitialIssuesUrl(\n options: SyncOptions,\n spec?: FetchSpec,\n ): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/issues/`,\n );\n u.searchParams.set(\n 'limit',\n String(clampPageSize(options.pageSize, ISSUES_PAGE_SIZE)),\n );\n u.searchParams.set('sort', 'date');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n const queryTerms: string[] = [];\n if (options.since) {\n queryTerms.push(`lastSeen:>${options.since}`);\n }\n queryTerms.push(...pushableIssueQueryTerms(spec?.filter));\n if (queryTerms.length > 0) {\n u.searchParams.set('query', queryTerms.join(' '));\n }\n return u.toString();\n }\n\n private singleSpec(\n options: SyncOptions,\n resource: string,\n ): FetchSpec | undefined {\n const specs = options.fetchSpecs?.[resource];\n return specs && specs.length === 1 ? specs[0] : undefined;\n }\n\n private buildInitialReleasesUrl(options: SyncOptions): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/releases/`,\n );\n u.searchParams.set(\n 'per_page',\n String(clampPageSize(options.pageSize, RELEASES_PAGE_SIZE)),\n );\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n return u.toString();\n }\n\n private buildStatsUrl(): string {\n const lookback =\n this.settings.statsLookbackHours ?? DEFAULT_STATS_LOOKBACK_HOURS;\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/stats_v2/`,\n );\n u.searchParams.set('field', 'sum(quantity)');\n u.searchParams.set('category', 'error');\n u.searchParams.set('interval', '1h');\n u.searchParams.set('statsPeriod', `${lookback}h`);\n u.searchParams.append('groupBy', 'project');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n return u.toString();\n }\n\n private buildIssueEventsUrl(issueId: string): string {\n const cap = this.settings.eventsPerIssueCap ?? DEFAULT_EVENTS_PER_ISSUE;\n const u = new URL(`${SENTRY_API_BASE}/issues/${issueId}/events/`);\n u.searchParams.set(\n 'limit',\n String(Math.min(cap, DEFAULT_EVENTS_PER_ISSUE)),\n );\n return u.toString();\n }\n\n private async fetchIssuesPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: IssuesPageItem[]; next: string | null }> {\n const url =\n page ??\n this.buildInitialIssuesUrl(\n options,\n this.singleSpec(options, 'sentry_issue'),\n );\n const res = await this.fetch<SentryIssue[]>(url, 'issues', signal);\n\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const next =\n nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('issues', nextLink.url)\n : null;\n\n const eventsByIssue = new Map<string, SentryIssueEvent[]>();\n if (this.isResourceEnabled('issue_events')) {\n signal?.throwIfAborted();\n const fetched = await mapWithConcurrency(\n res.body,\n EVENT_FETCH_CONCURRENCY,\n async (issue) => {\n const eventsRes = await this.fetch<SentryIssueEvent[]>(\n this.buildIssueEventsUrl(issue.id),\n 'issue_events',\n signal,\n );\n return [issue.id, eventsRes.body] as const;\n },\n );\n for (const [issueId, events] of fetched) {\n eventsByIssue.set(issueId, events);\n }\n }\n\n return { items: [{ issues: res.body, eventsByIssue }], next };\n }\n\n private async fetchReleasesPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryRelease[]; next: string | null }> {\n const url = page ?? this.buildInitialReleasesUrl(options);\n const res = await this.fetch<SentryRelease[]>(url, 'releases', signal);\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const releases = res.body;\n const cutoff = options.since ? new Date(options.since).getTime() : null;\n const filtered =\n cutoff !== null\n ? releases.filter((r) => {\n const ts = new Date(r.dateReleased ?? r.dateCreated).getTime();\n return Number.isFinite(ts) ? ts >= cutoff : true;\n })\n : releases;\n const lastRelease = releases.at(-1);\n const lastTs = lastRelease\n ? new Date(lastRelease.dateReleased ?? lastRelease.dateCreated).getTime()\n : null;\n const cutoffReached =\n cutoff !== null &&\n lastTs !== null &&\n Number.isFinite(lastTs) &&\n lastTs < cutoff;\n const next =\n !cutoffReached && nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('releases', nextLink.url)\n : null;\n return { items: filtered, next };\n }\n\n private async fetchErrorStats(\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryStatsResponse[]; next: string | null }> {\n const res = await this.fetch<SentryStatsResponse>(\n this.buildStatsUrl(),\n 'error_stats',\n signal,\n );\n return { items: [res.body], next: null };\n }\n\n private async writeIssuesPage(\n storage: StorageHandle,\n item: IssuesPageItem,\n ): Promise<void> {\n const writeEntities = this.isResourceEnabled('issues');\n const writeEvents = this.isResourceEnabled('issue_events');\n\n for (const issue of item.issues) {\n if (writeEntities) {\n const count =\n typeof issue.count === 'string' ? Number(issue.count) : issue.count;\n const firstSeenMs = parseEpoch(issue.firstSeen, 'iso');\n const lastSeenMs = parseEpoch(issue.lastSeen, 'iso');\n if (firstSeenMs === null || lastSeenMs === null) {\n console.warn(\n `[connector-sentry] skipping issue ${issue.id} with unparseable firstSeen/lastSeen`,\n );\n } else {\n await storage.entity({\n type: 'sentry_issue',\n id: issue.id,\n attributes: {\n shortId: issue.shortId,\n title: issue.title,\n level: issue.level,\n status: issue.status,\n firstSeen: firstSeenMs,\n lastSeen: lastSeenMs,\n count: Number.isFinite(count) ? count : 0,\n userCount: issue.userCount,\n projectSlug: issue.project.slug,\n },\n updated_at: lastSeenMs,\n });\n }\n }\n\n if (writeEvents) {\n const events = item.eventsByIssue.get(issue.id) ?? [];\n for (const ev of events) {\n const eventId = ev.eventID ?? ev.id ?? null;\n if (eventId === null) {\n continue;\n }\n const startTs = parseEpoch(ev.dateCreated, 'iso');\n if (startTs === null) {\n continue;\n }\n await storage.event({\n name: 'sentry_issue_event',\n start_ts: startTs,\n end_ts: null,\n attributes: {\n eventId,\n issueId: issue.id,\n issueShortId: issue.shortId,\n projectSlug: issue.project.slug,\n level: issue.level,\n platform: ev.platform ?? null,\n environment: ev.environment ?? null,\n message: ev.message ?? null,\n },\n });\n }\n }\n }\n }\n\n private async writeReleases(\n storage: StorageHandle,\n releases: SentryRelease[],\n ): Promise<void> {\n for (const r of releases) {\n const createdMs = parseEpoch(r.dateCreated, 'iso');\n const releasedMs = parseEpoch(r.dateReleased, 'iso');\n const lastEventMs = parseEpoch(r.lastEvent, 'iso');\n if (createdMs === null) {\n console.warn(\n `[connector-sentry] skipping release ${r.version} with unparseable dateCreated`,\n );\n continue;\n }\n await storage.entity({\n type: 'sentry_release',\n id: r.version,\n attributes: {\n version: r.version,\n projects: r.projects.map((p) => p.slug),\n dateCreated: createdMs,\n dateReleased: releasedMs,\n lastEvent: lastEventMs,\n },\n updated_at: Math.max(createdMs, releasedMs ?? 0, lastEventMs ?? 0),\n });\n }\n }\n\n private async writeErrorStats(\n storage: StorageHandle,\n stats: SentryStatsResponse,\n ): Promise<void> {\n const samples: Array<{\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number>;\n }> = [];\n const intervals = stats.intervals ?? [];\n for (const group of stats.groups) {\n const project = group.by['project'];\n const projectKey = project !== undefined ? String(project) : 'unknown';\n const series = group.series?.['sum(quantity)'] ?? [];\n if (series.length === 0) {\n continue;\n }\n for (let i = 0; i < intervals.length; i++) {\n const intervalIso = intervals[i];\n const rawValue = series[i];\n if (intervalIso === undefined || rawValue === undefined) {\n continue;\n }\n const ts = parseEpoch(intervalIso, 'iso');\n const value = Number(rawValue);\n if (ts === null || !Number.isFinite(value)) {\n continue;\n }\n samples.push({\n name: 'sentry_errors_per_hour',\n ts,\n value,\n attributes: { project: projectKey },\n });\n }\n }\n await storage.metrics(samples, { names: ['sentry_errors_per_hour'] });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = this.resolveCursor(options.cursor);\n const isFull = options.mode === 'full';\n const phases = this.activePhases();\n\n return paginateChunked<SentryPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n pipeline: true,\n maxChunkMs: CHUNK_BUDGET_MS,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'issues':\n return this.fetchIssuesPage(page, options, sig);\n case 'releases':\n return this.fetchReleasesPage(page, options, sig);\n case 'error_stats':\n return this.fetchErrorStats(sig);\n }\n },\n writeBatch: async (phase, items, page) => {\n if (isFull && page === null) {\n switch (phase) {\n case 'issues':\n if (this.isResourceEnabled('issues')) {\n await storage.entities([], { types: ['sentry_issue'] });\n }\n if (this.isResourceEnabled('issue_events')) {\n await storage.events([], { names: ['sentry_issue_event'] });\n }\n break;\n case 'releases':\n await storage.entities([], { types: ['sentry_release'] });\n break;\n case 'error_stats':\n break;\n }\n }\n switch (phase) {\n case 'issues':\n for (const item of items as IssuesPageItem[]) {\n await this.writeIssuesPage(storage, item);\n }\n return;\n case 'releases':\n return this.writeReleases(storage, items as SentryRelease[]);\n case 'error_stats':\n for (const stats of items as SentryStatsResponse[]) {\n await this.writeErrorStats(storage, stats);\n }\n return;\n }\n },\n });\n }\n}\n","import { SentryConnector } from './sentry';\n\nexport {\n configFields,\n doc,\n SentryConnector,\n sentryResources as resources,\n id,\n} from './sentry';\nexport type { SentryResource, SentrySettings } from './sentry';\nexport default SentryConnector;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/sentry.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n mapWithConcurrency,\n parseEpoch,\n sanitizeAllowedUrl,\n standardRateLimitPolicy,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type FetchSpec,\n type FilterClause,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n authToken: z.object({ $secret: z.string() }).meta({\n label: 'Auth Token',\n description:\n 'Sentry Internal Integration token or User Auth Token. Create one at Sentry → Settings → Auth Tokens (or for an org, Settings → Custom Integrations → New Internal Integration).',\n placeholder: 'sntrys_...',\n secret: true,\n }),\n organization: z.string().min(1).meta({\n label: 'Organization slug',\n description: \"Your Sentry organization's slug, as it appears in the URL.\",\n placeholder: 'acme',\n }),\n projects: z.array(z.string().min(1)).nonempty().optional().meta({\n label: 'Projects (optional)',\n description:\n 'Restrict the sync to specific Sentry project slugs (or numeric IDs). Omit to sync every project the token can see.',\n }),\n resources: z\n .array(z.enum(['issues', 'issue_events', 'releases', 'errors_per_hour']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n \"Which Sentry resources to sync. Omit to sync all of them. 'issue_events' depends on 'issues' being fetched - enabling it without 'issues' still runs the issues query, but skips writing issue entities.\",\n }),\n eventsPerIssueCap: z.number().int().positive().max(100).optional().meta({\n label: 'Events per issue cap',\n description:\n 'Maximum number of recent events (occurrences) to sample per issue on each sync. Defaults to 100 (the max page size Sentry allows for the issue events endpoint).',\n placeholder: '100',\n }),\n statsLookbackHours: z.number().int().positive().max(168).optional().meta({\n label: 'Stats lookback (hours)',\n description:\n 'How many hours of hourly error-rate data to refresh on each sync. Defaults to 24.',\n placeholder: '24',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Sentry',\n category: 'engineering',\n brandColor: '#362D59',\n tagline:\n 'Sync issues, issue events, releases, and hourly error rates from a Sentry organization.',\n vendor: {\n name: 'Sentry',\n domain: 'sentry.io',\n apiDocs: 'https://docs.sentry.io/api/',\n website: 'https://sentry.io',\n },\n auth: {\n summary:\n 'A Sentry auth token is required. Use an organization-level Internal Integration token or a User Auth Token with read access to issues, events, and releases.',\n setup: [\n 'Open Sentry → Settings → Custom Integrations → New Internal Integration (or Settings → Auth Tokens for a personal token).',\n 'Grant read access to Issues & Events and Releases.',\n 'Copy the generated token and store it as a secret, referencing it from the connector config as `authToken: secret(\"SENTRY_AUTH_TOKEN\")`.',\n 'Set the `organization` slug as it appears in your Sentry URL.',\n ],\n },\n rateLimit:\n 'Sentry returns X-Sentry-Rate-Limit-Remaining / X-Sentry-Rate-Limit-Reset headers (reset in seconds); list pagination uses the Link header (page size 100).',\n limitations: [\n 'Performance / trace data is out of scope (high cost, low signal for dashboards).',\n 'Self-hosted Sentry on custom hosts is out of scope (pagination URLs are pinned to sentry.io).',\n ],\n});\n\nexport type SentryResource =\n | 'issues'\n | 'issue_events'\n | 'releases'\n | 'errors_per_hour';\n\nexport interface SentrySettings {\n organization: string;\n projects?: readonly string[];\n resources?: readonly SentryResource[];\n eventsPerIssueCap?: number;\n statsLookbackHours?: number;\n}\n\nconst sentryCredentials = {\n authToken: {\n description: 'Sentry auth token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype SentryCredentials = typeof sentryCredentials;\n\nconst sentryRateLimit = standardRateLimitPolicy({\n remainingHeader: 'x-sentry-rate-limit-remaining',\n resetHeader: 'x-sentry-rate-limit-reset',\n resetUnit: 's',\n});\n\nconst PHASE_ORDER = ['issues', 'releases', 'error_stats'] as const;\n\ntype SentryPhase = (typeof PHASE_ORDER)[number];\n\ntype SentrySyncCursor = ChunkedSyncCursor<SentryPhase, string>;\n\nconst isSentrySyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\ninterface SentryProjectRef {\n id?: string | number;\n slug: string;\n name?: string;\n platform?: string;\n}\n\ninterface SentryIssue {\n id: string;\n shortId: string;\n title: string;\n level: string;\n status: string;\n firstSeen: string;\n lastSeen: string;\n count: string | number;\n userCount: number;\n project: SentryProjectRef;\n}\n\ninterface SentryIssueEvent {\n id?: string;\n eventID?: string;\n dateCreated: string;\n message?: string | null;\n platform?: string | null;\n groupID?: string;\n environment?: string | null;\n}\n\ninterface SentryRelease {\n version: string;\n dateCreated: string;\n dateReleased?: string | null;\n lastEvent?: string | null;\n projects: SentryProjectRef[];\n}\n\ninterface SentryStatsResponse {\n intervals?: string[];\n groups: Array<{\n by: Record<string, string | number>;\n totals?: Record<string, number>;\n series?: Record<string, number[]>;\n }>;\n start?: string;\n end?: string;\n}\n\ninterface IssuesPageItem {\n issues: SentryIssue[];\n eventsByIssue: Map<string, SentryIssueEvent[]>;\n}\n\ninterface SentryLink {\n url: string;\n hasResults: boolean;\n}\n\nfunction parseSentryLink(\n header: string | null,\n rel: string,\n): SentryLink | null {\n if (!header) {\n return null;\n }\n for (const part of header.split(',')) {\n const m = part.match(/<([^>]+)>\\s*;\\s*(.+)$/);\n if (!m) {\n continue;\n }\n const url = m[1]!;\n const attrs = m[2]!;\n const relMatch = attrs.match(/rel=\"([^\"]+)\"/);\n if (!relMatch || relMatch[1] !== rel) {\n continue;\n }\n const resultsMatch = attrs.match(/results=\"([^\"]+)\"/);\n const hasResults = resultsMatch ? resultsMatch[1] === 'true' : true;\n return { url, hasResults };\n }\n return null;\n}\n\nconst SENTRY_API_HOST = 'sentry.io';\nconst SENTRY_API_BASE = `https://${SENTRY_API_HOST}/api/0`;\nconst DEFAULT_EVENTS_PER_ISSUE = 100;\nconst DEFAULT_STATS_LOOKBACK_HOURS = 24;\nconst MAX_PAGE_SIZE = 100;\nconst ISSUES_PAGE_SIZE = 100;\nconst RELEASES_PAGE_SIZE = 100;\nconst EVENT_FETCH_CONCURRENCY = 5;\nconst CHUNK_BUDGET_MS = 25_000;\n\nfunction clampPageSize(\n requested: number | undefined,\n fallback: number,\n): number {\n const n = requested ?? fallback;\n if (!Number.isFinite(n) || n < 1) {\n return 1;\n }\n return Math.min(Math.floor(n), MAX_PAGE_SIZE);\n}\n\nconst idString = z.string().min(1);\n\nconst issueResponseSchema = z.array(\n z.object({\n id: idString,\n shortId: z.string(),\n title: z.string(),\n level: z.enum(['debug', 'info', 'warning', 'error', 'fatal']),\n status: z.enum(['resolved', 'unresolved', 'ignored']),\n firstSeen: z.iso.datetime(),\n lastSeen: z.iso.datetime(),\n count: z.union([z.string().regex(/^\\d+$/), z.number().int().nonnegative()]),\n userCount: z.number().int().nonnegative(),\n project: z.object({\n slug: z.string().min(1),\n id: z.union([idString, z.number()]).optional(),\n name: z.string().optional(),\n platform: z.string().nullable().optional(),\n }),\n annotations: z.unknown().optional(),\n assignedTo: z.unknown().nullable().optional(),\n culprit: z.string().nullable().optional(),\n filtered: z.unknown().nullable().optional(),\n hasSeen: z.boolean().optional(),\n isBookmarked: z.boolean().optional(),\n isPublic: z.boolean().optional(),\n isSubscribed: z.boolean().optional(),\n isUnhandled: z.boolean().optional(),\n issueCategory: z.string().optional(),\n issueType: z.string().optional(),\n lifetime: z.unknown().optional(),\n logger: z.string().nullable().optional(),\n metadata: z.unknown().optional(),\n numComments: z.number().int().nonnegative().optional(),\n permalink: z.string().optional(),\n platform: z.string().nullable().optional(),\n priority: z.string().optional(),\n priorityLockedAt: z.iso.datetime().nullable().optional(),\n seerAutofixLastTriggered: z.iso.datetime().nullable().optional(),\n seerExplorerAutofixLastTriggered: z.iso.datetime().nullable().optional(),\n seerFixabilityScore: z.number().nullable().optional(),\n shareId: z.string().nullable().optional(),\n stats: z.unknown().optional(),\n statusDetails: z.unknown().optional(),\n subscriptionDetails: z.unknown().nullable().optional(),\n substatus: z.string().nullable().optional(),\n type: z.string().optional(),\n }),\n);\n\nconst issueEventResponseSchema = z.array(\n z.object({\n id: z.string().optional(),\n eventID: z.string().optional(),\n dateCreated: z.iso.datetime(),\n message: z.string().nullable().optional(),\n platform: z.string().nullable().optional(),\n groupID: z.string().optional(),\n environment: z.string().nullable().optional(),\n crashFile: z.unknown().nullable().optional(),\n culprit: z.string().optional(),\n 'event.type': z.string().optional(),\n location: z.string().optional(),\n metadata: z.unknown().optional(),\n projectID: z.string().optional(),\n tags: z.unknown().optional(),\n title: z.string().optional(),\n user: z.unknown().optional(),\n }),\n);\n\nconst releaseResponseSchema = z.array(\n z.object({\n version: idString,\n dateCreated: z.iso.datetime(),\n dateReleased: z.iso.datetime().nullable().optional(),\n lastEvent: z.iso.datetime().nullable().optional(),\n projects: z.array(\n z.object({\n slug: z.string().min(1),\n hasHealthData: z.unknown().optional(),\n id: z.unknown().optional(),\n name: z.unknown().optional(),\n newGroups: z.unknown().optional(),\n platform: z.unknown().optional(),\n platforms: z.unknown().optional(),\n }),\n ),\n authors: z.unknown().optional(),\n commitCount: z.unknown().optional(),\n currentProjectMeta: z.unknown().optional(),\n data: z.unknown().optional(),\n deployCount: z.unknown().optional(),\n firstEvent: z.unknown().nullable().optional(),\n id: z.unknown().optional(),\n lastCommit: z.unknown().optional(),\n lastDeploy: z.unknown().optional(),\n newGroups: z.unknown().optional(),\n owner: z.unknown().optional(),\n ref: z.unknown().nullable().optional(),\n shortVersion: z.unknown().optional(),\n status: z.unknown().optional(),\n url: z.unknown().nullable().optional(),\n userAgent: z.unknown().nullable().optional(),\n versionInfo: z.unknown().optional(),\n }),\n);\n\nconst errorStatsResponseSchema = z.object({\n intervals: z.array(z.iso.datetime()).optional(),\n groups: z.array(\n z.object({\n by: z.record(z.string(), z.union([z.string(), z.number()])),\n totals: z.record(z.string(), z.number()).optional(),\n series: z.record(z.string(), z.array(z.number())).optional(),\n }),\n ),\n start: z.string().optional(),\n end: z.string().optional(),\n});\n\nexport const sentryResources = defineResources({\n sentry_issue: {\n shape: 'entity',\n description:\n 'Sentry issues (error groups) with level, status, occurrence count, affected user count, and first/last seen timestamps.',\n endpoint: 'GET /api/0/organizations/{organization}/issues/',\n filterable: [\n {\n field: 'status',\n ops: ['eq'],\n values: ['resolved', 'unresolved', 'ignored'],\n },\n {\n field: 'level',\n ops: ['eq'],\n values: ['fatal', 'error', 'warning', 'info', 'debug', 'sample'],\n },\n ],\n responses: { issues: issueResponseSchema },\n },\n sentry_issue_event: {\n shape: 'event',\n description:\n 'Individual event occurrences sampled per issue, with platform, environment, level, and message.',\n endpoint: 'GET /api/0/issues/{issueId}/events/',\n notes:\n 'Events are sampled: at most eventsPerIssueCap recent events per issue per sync (Sentry caps a single events page at 100), so this is a representative sample, not a full audit trail.',\n filterable: [],\n responses: { issue_events: issueEventResponseSchema },\n },\n sentry_release: {\n shape: 'entity',\n description:\n 'Releases with their versions, associated project slugs, and creation/release/last-event timestamps.',\n endpoint: 'GET /api/0/organizations/{organization}/releases/',\n filterable: [],\n responses: { releases: releaseResponseSchema },\n },\n sentry_errors_per_hour: {\n shape: 'metric',\n description:\n 'Hourly count of accepted (stored) error events, broken down by project, over the configured lookback window.',\n endpoint: 'GET /api/0/organizations/{organization}/stats_v2/',\n unit: 'errors',\n granularity: '1h',\n dimensions: [\n { name: 'project', description: 'Sentry project slug or id.' },\n ],\n responses: { error_stats: errorStatsResponseSchema },\n },\n});\n\nexport const id = 'sentry';\n\nfunction pushableIssueQueryTerms(filter: FilterClause[] | undefined): string[] {\n if (!filter) {\n return [];\n }\n const terms: string[] = [];\n for (const clause of filter) {\n if (!('field' in clause) || clause.op !== 'eq') {\n continue;\n }\n if (clause.field === 'status' && typeof clause.value === 'string') {\n terms.push(`is:${clause.value}`);\n } else if (clause.field === 'level' && typeof clause.value === 'string') {\n terms.push(`level:${clause.value}`);\n }\n }\n return terms;\n}\n\nexport class SentryConnector extends BaseConnector<\n SentrySettings,\n SentryCredentials\n> {\n static readonly id = id;\n\n static readonly resources = sentryResources;\n\n static readonly schemas = schemasFromResources(sentryResources);\n\n static create(input: unknown, ctx?: ConnectorContext): SentryConnector {\n const parsed = configFields.parse(input);\n return new SentryConnector(\n {\n organization: parsed.organization,\n projects: parsed.projects,\n resources: parsed.resources,\n eventsPerIssueCap: parsed.eventsPerIssueCap,\n statsLookbackHours: parsed.statsLookbackHours,\n },\n { authToken: parsed.authToken },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = sentryCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.authToken}`,\n 'User-Agent': connectorUserAgent('sentry'),\n };\n }\n\n private fetch<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n rateLimit: sentryRateLimit,\n });\n }\n\n private activePhases(): SentryPhase[] {\n return selectActivePhases<SentryResource, SentryPhase>(\n (r) => {\n switch (r) {\n case 'issues':\n case 'issue_events':\n return 'issues';\n case 'releases':\n return 'releases';\n case 'errors_per_hour':\n return 'error_stats';\n }\n },\n PHASE_ORDER,\n this.settings.resources,\n );\n }\n\n private allowedPagePath(phase: SentryPhase): string | null {\n const org = this.settings.organization;\n switch (phase) {\n case 'issues':\n return `/api/0/organizations/${org}/issues/`;\n case 'releases':\n return `/api/0/organizations/${org}/releases/`;\n case 'error_stats':\n return null;\n }\n }\n\n private sanitizePageUrl(\n phase: SentryPhase,\n pageUrl: string | null,\n ): string | null {\n const allowedPath = this.allowedPagePath(phase);\n if (allowedPath === null) {\n return null;\n }\n return sanitizeAllowedUrl({\n url: pageUrl,\n host: SENTRY_API_HOST,\n pathname: allowedPath,\n });\n }\n\n private resolveCursor(cursor: unknown): SentrySyncCursor | undefined {\n if (!isSentrySyncCursor(cursor)) {\n return undefined;\n }\n return {\n phase: cursor.phase,\n page: this.sanitizePageUrl(cursor.phase, cursor.page),\n };\n }\n\n private buildInitialIssuesUrl(\n options: SyncOptions,\n spec?: FetchSpec,\n ): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/issues/`,\n );\n u.searchParams.set(\n 'limit',\n String(clampPageSize(options.pageSize, ISSUES_PAGE_SIZE)),\n );\n u.searchParams.set('sort', 'date');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n const queryTerms: string[] = [];\n if (options.since) {\n queryTerms.push(`lastSeen:>${options.since}`);\n }\n queryTerms.push(...pushableIssueQueryTerms(spec?.filter));\n if (queryTerms.length > 0) {\n u.searchParams.set('query', queryTerms.join(' '));\n }\n return u.toString();\n }\n\n private singleSpec(\n options: SyncOptions,\n resource: string,\n ): FetchSpec | undefined {\n const specs = options.fetchSpecs?.[resource];\n return specs && specs.length === 1 ? specs[0] : undefined;\n }\n\n private buildInitialReleasesUrl(options: SyncOptions): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/releases/`,\n );\n u.searchParams.set(\n 'per_page',\n String(clampPageSize(options.pageSize, RELEASES_PAGE_SIZE)),\n );\n u.searchParams.set('sort', 'date');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n return u.toString();\n }\n\n private buildStatsUrl(): string {\n const lookback =\n this.settings.statsLookbackHours ?? DEFAULT_STATS_LOOKBACK_HOURS;\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/stats_v2/`,\n );\n u.searchParams.set('field', 'sum(quantity)');\n u.searchParams.set('category', 'error');\n u.searchParams.set('outcome', 'accepted');\n u.searchParams.set('interval', '1h');\n u.searchParams.set('statsPeriod', `${lookback}h`);\n u.searchParams.append('groupBy', 'project');\n const projects = this.settings.projects ?? [];\n if (projects.length > 0) {\n for (const project of projects) {\n u.searchParams.append('project', project);\n }\n } else {\n u.searchParams.append('project', '-1');\n }\n return u.toString();\n }\n\n private buildIssueEventsUrl(issueId: string): string {\n const cap = this.settings.eventsPerIssueCap ?? DEFAULT_EVENTS_PER_ISSUE;\n const u = new URL(`${SENTRY_API_BASE}/issues/${issueId}/events/`);\n u.searchParams.set(\n 'limit',\n String(Math.min(cap, DEFAULT_EVENTS_PER_ISSUE)),\n );\n return u.toString();\n }\n\n private async fetchIssuesPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: IssuesPageItem[]; next: string | null }> {\n const url =\n page ??\n this.buildInitialIssuesUrl(\n options,\n this.singleSpec(options, 'sentry_issue'),\n );\n const res = await this.fetch<SentryIssue[]>(url, 'issues', signal);\n\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const next =\n nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('issues', nextLink.url)\n : null;\n\n const eventsByIssue = new Map<string, SentryIssueEvent[]>();\n if (this.isResourceEnabled('issue_events')) {\n signal?.throwIfAborted();\n const fetched = await mapWithConcurrency(\n res.body,\n EVENT_FETCH_CONCURRENCY,\n async (issue) => {\n const eventsRes = await this.fetch<SentryIssueEvent[]>(\n this.buildIssueEventsUrl(issue.id),\n 'issue_events',\n signal,\n );\n return [issue.id, eventsRes.body] as const;\n },\n );\n for (const [issueId, events] of fetched) {\n eventsByIssue.set(issueId, events);\n }\n }\n\n return { items: [{ issues: res.body, eventsByIssue }], next };\n }\n\n private async fetchReleasesPage(\n page: string | null,\n options: SyncOptions,\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryRelease[]; next: string | null }> {\n const url = page ?? this.buildInitialReleasesUrl(options);\n const res = await this.fetch<SentryRelease[]>(url, 'releases', signal);\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const releases = res.body;\n const cutoff = options.since ? new Date(options.since).getTime() : null;\n const filtered =\n cutoff !== null\n ? releases.filter((r) => {\n const ts = new Date(r.dateCreated).getTime();\n return Number.isFinite(ts) ? ts >= cutoff : true;\n })\n : releases;\n const lastRelease = releases.at(-1);\n const lastTs = lastRelease\n ? new Date(lastRelease.dateCreated).getTime()\n : null;\n const cutoffReached =\n cutoff !== null &&\n lastTs !== null &&\n Number.isFinite(lastTs) &&\n lastTs < cutoff;\n const next =\n !cutoffReached && nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('releases', nextLink.url)\n : null;\n return { items: filtered, next };\n }\n\n private async fetchErrorStats(\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryStatsResponse[]; next: string | null }> {\n const res = await this.fetch<SentryStatsResponse>(\n this.buildStatsUrl(),\n 'error_stats',\n signal,\n );\n return { items: [res.body], next: null };\n }\n\n private async writeIssuesPage(\n storage: StorageHandle,\n item: IssuesPageItem,\n ): Promise<void> {\n const writeEntities = this.isResourceEnabled('issues');\n const writeEvents = this.isResourceEnabled('issue_events');\n\n for (const issue of item.issues) {\n if (writeEntities) {\n const count =\n typeof issue.count === 'string' ? Number(issue.count) : issue.count;\n const firstSeenMs = parseEpoch(issue.firstSeen, 'iso');\n const lastSeenMs = parseEpoch(issue.lastSeen, 'iso');\n if (firstSeenMs === null || lastSeenMs === null) {\n console.warn(\n `[connector-sentry] skipping issue ${issue.id} with unparseable firstSeen/lastSeen`,\n );\n } else {\n await storage.entity({\n type: 'sentry_issue',\n id: issue.id,\n attributes: {\n shortId: issue.shortId,\n title: issue.title,\n level: issue.level,\n status: issue.status,\n firstSeen: firstSeenMs,\n lastSeen: lastSeenMs,\n count: Number.isFinite(count) ? count : 0,\n userCount: issue.userCount,\n projectSlug: issue.project.slug,\n },\n updated_at: lastSeenMs,\n });\n }\n }\n\n if (writeEvents) {\n const events = item.eventsByIssue.get(issue.id) ?? [];\n for (const ev of events) {\n const eventId = ev.eventID ?? ev.id ?? null;\n if (eventId === null) {\n continue;\n }\n const startTs = parseEpoch(ev.dateCreated, 'iso');\n if (startTs === null) {\n continue;\n }\n await storage.event({\n name: 'sentry_issue_event',\n start_ts: startTs,\n end_ts: null,\n attributes: {\n eventId,\n issueId: issue.id,\n issueShortId: issue.shortId,\n projectSlug: issue.project.slug,\n level: issue.level,\n platform: ev.platform ?? null,\n environment: ev.environment ?? null,\n message: ev.message ?? null,\n },\n });\n }\n }\n }\n }\n\n private async writeReleases(\n storage: StorageHandle,\n releases: SentryRelease[],\n ): Promise<void> {\n for (const r of releases) {\n const createdMs = parseEpoch(r.dateCreated, 'iso');\n const releasedMs = parseEpoch(r.dateReleased, 'iso');\n const lastEventMs = parseEpoch(r.lastEvent, 'iso');\n if (createdMs === null) {\n console.warn(\n `[connector-sentry] skipping release ${r.version} with unparseable dateCreated`,\n );\n continue;\n }\n await storage.entity({\n type: 'sentry_release',\n id: r.version,\n attributes: {\n version: r.version,\n projects: r.projects.map((p) => p.slug),\n dateCreated: createdMs,\n dateReleased: releasedMs,\n lastEvent: lastEventMs,\n },\n updated_at: Math.max(createdMs, releasedMs ?? 0, lastEventMs ?? 0),\n });\n }\n }\n\n private async writeErrorStats(\n storage: StorageHandle,\n stats: SentryStatsResponse,\n ): Promise<void> {\n const samples: Array<{\n name: string;\n ts: number;\n value: number;\n attributes: Record<string, string | number>;\n }> = [];\n\n let intervals = stats.intervals ?? [];\n if (intervals.length === 0 && stats.start) {\n const seriesLen = stats.groups.reduce((max, group) => {\n const len = group.series?.['sum(quantity)']?.length ?? 0;\n return Math.max(max, len);\n }, 0);\n const startMs = parseEpoch(stats.start, 'iso');\n if (seriesLen > 0 && startMs !== null) {\n intervals = Array.from({ length: seriesLen }, (_, i) =>\n new Date(startMs + i * 3_600_000).toISOString(),\n );\n }\n }\n\n for (const group of stats.groups) {\n const project = group.by['project'];\n const projectKey = project !== undefined ? String(project) : 'unknown';\n const series = group.series?.['sum(quantity)'] ?? [];\n for (let i = 0; i < intervals.length; i++) {\n const intervalIso = intervals[i];\n if (intervalIso === undefined) {\n continue;\n }\n const ts = parseEpoch(intervalIso, 'iso');\n if (ts === null) {\n continue;\n }\n const rawValue = series[i];\n const value = rawValue === undefined ? 0 : Number(rawValue);\n if (!Number.isFinite(value)) {\n continue;\n }\n samples.push({\n name: 'sentry_errors_per_hour',\n ts,\n value,\n attributes: { project: projectKey },\n });\n }\n }\n await storage.metrics(samples, { names: ['sentry_errors_per_hour'] });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = this.resolveCursor(options.cursor);\n const isFull = options.mode === 'full';\n const phases = this.activePhases();\n\n return paginateChunked<SentryPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n pipeline: true,\n maxChunkMs: CHUNK_BUDGET_MS,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'issues':\n return this.fetchIssuesPage(page, options, sig);\n case 'releases':\n return this.fetchReleasesPage(page, options, sig);\n case 'error_stats':\n return this.fetchErrorStats(sig);\n }\n },\n writeBatch: async (phase, items, page) => {\n if (isFull && page === null) {\n switch (phase) {\n case 'issues':\n if (this.isResourceEnabled('issues')) {\n await storage.entities([], { types: ['sentry_issue'] });\n }\n if (this.isResourceEnabled('issue_events')) {\n await storage.events([], { names: ['sentry_issue_event'] });\n }\n break;\n case 'releases':\n await storage.entities([], { types: ['sentry_release'] });\n break;\n case 'error_stats':\n break;\n }\n }\n switch (phase) {\n case 'issues':\n for (const item of items as IssuesPageItem[]) {\n await this.writeIssuesPage(storage, item);\n }\n return;\n case 'releases':\n return this.writeReleases(storage, items as SentryRelease[]);\n case 'error_stats':\n for (const stats of items as SentryStatsResponse[]) {\n await this.writeErrorStats(storage, stats);\n }\n return;\n }\n },\n });\n }\n}\n","import { SentryConnector } from './sentry';\n\nexport {\n configFields,\n doc,\n SentryConnector,\n sentryResources as resources,\n id,\n} from './sentry';\nexport type { SentryResource, SentrySettings } from './sentry';\nexport default SentryConnector;\n"],"mappings":";AEAO,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB,qBAAqB,mBAAmB;AAEnE,SAAS,mBAAmB,aAA6B;AAC9D,SAAO,qBAAqB,WAAW,IAAI,mBAAmB;AAChE;AEUO,SAAS,wBACd,QACiB;AACjB,QAAM,EAAE,iBAAiB,aAAa,WAAW,gBAAgB,IAAI;AACrE,QAAM,aAAa,cAAc,MAAM,MAAO;AAC9C,SAAO;IACL,MAAM,GAAG;AACP,YAAM,eAAe,EAAE,IAAI,eAAe;AAC1C,UAAI,iBAAiB,QAAQ,aAAa,KAAK,MAAM,IAAI;AACvD,eAAO;MACT;AACA,YAAM,YAAY,OAAO,YAAY;AACrC,UAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC/B,eAAO;MACT;AACA,YAAM,WAAW,EAAE,IAAI,WAAW;AAClC,UAAI,aAAa,MAAM;AACrB,YAAI,oBAAoB,QAAW;AACjC,iBAAO;QACT;AACA,eAAO;UACL;UACA,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe;QAChD;MACF;AACA,UAAI,SAAS,KAAK,MAAM,IAAI;AAC1B,eAAO;MACT;AACA,YAAM,QAAQ,OAAO,QAAQ;AAC7B,UAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,GAAG;AACxC,eAAO;MACT;AACA,YAAM,UAAU,QAAQ;AACxB,UAAI,CAAC,OAAO,SAAS,OAAO,GAAG;AAC7B,eAAO;MACT;AACA,aAAO,EAAE,WAAW,SAAS,IAAI,KAAK,OAAO,EAAE;IACjD;EACF;AACF;ACvDA,eAAsB,mBACpB,OACA,aACA,IACc;AACd,QAAM,UAAU,IAAI,MAAS,MAAM,MAAM;AACzC,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO;EACT;AACA,QAAM,aAAa,OAAO,SAAS,WAAW,IAAI,KAAK,MAAM,WAAW,IAAI;AAC5E,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,YAAY,MAAM,MAAM,CAAC;AAC5D,MAAI,OAAO;AACX,MAAI,SAAS;AAEb,iBAAe,SAAwB;AACrC,WAAO,CAAC,QAAQ;AACd,YAAM,IAAI;AACV,UAAI,KAAK,MAAM,QAAQ;AACrB;MACF;AACA,UAAI;AACF,gBAAQ,CAAC,IAAI,MAAM,GAAG,MAAM,CAAC,GAAI,CAAC;MACpC,SAAS,KAAK;AACZ,iBAAS;AACT,cAAM;MACR;IACF;EACF;AAEA,QAAM,UAA2B,CAAC;AAClC,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAQ,KAAK,OAAO,CAAC;EACvB;AACA,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AC5BO,SAAS,mBACd,SACe;AACf,QAAM,EAAE,KAAK,MAAM,UAAU,WAAW,SAAS,IAAI;AACrD,MAAI,QAAQ,MAAM;AAChB,WAAO;EACT;AACA,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,QAAI,EAAE,aAAa,YAAY,EAAE,SAAS,QAAQ,EAAE,aAAa,UAAU;AACzE,aAAO;IACT;AACA,WAAO,EAAE,SAAS;EACpB,QAAQ;AACN,WAAO;EACT;AACF;ACrBO,SAAS,WACd,OACA,MACe;AACf,MAAI,UAAU,QAAQ,UAAU,QAAW;AACzC,WAAO;EACT;AACA,MAAI,SAAS,OAAO;AAClB,QAAI,OAAO,UAAU,UAAU;AAC7B,aAAO;IACT;AACA,UAAM,KAAK,IAAI,KAAK,KAAK,EAAE,QAAQ;AACnC,WAAO,OAAO,SAAS,EAAE,IAAI,KAAK;EACpC;AACA,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,IAAI;AACpD,WAAO;EACT;AACA,QAAM,IAAI,OAAO,UAAU,WAAW,QAAQ,OAAO,KAAK;AAC1D,MAAI,CAAC,OAAO,SAAS,CAAC,GAAG;AACvB,WAAO;EACT;AACA,QAAM,SAAS,SAAS,MAAM,IAAI,MAAO;AACzC,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;;;AGjBA;AAAA,EACE;AAAA,EAUA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,SAAS;AAEX,IAAM,eAAe;AAAA,EAC1B,EAAE,OAAO;AAAA,IACP,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK;AAAA,MAChD,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,KAAK;AAAA,MACnC,OAAO;AAAA,MACP,aAAa;AAAA,MACb,aAAa;AAAA,IACf,CAAC;AAAA,IACD,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK;AAAA,MAC9D,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACD,WAAW,EACR,MAAM,EAAE,KAAK,CAAC,UAAU,gBAAgB,YAAY,iBAAiB,CAAC,CAAC,EACvE,SAAS,EACT,SAAS,EACT,KAAK;AAAA,MACJ,OAAO;AAAA,MACP,aACE;AAAA,IACJ,CAAC;AAAA,IACH,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,KAAK;AAAA,MACtE,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,IACf,CAAC;AAAA,IACD,oBAAoB,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,EAAE,SAAS,EAAE,KAAK;AAAA,MACvE,OAAO;AAAA,MACP,aACE;AAAA,MACF,aAAa;AAAA,IACf,CAAC;AAAA,EACH,CAAC;AACH;AAEO,IAAM,MAAoB,mBAAmB;AAAA,EAClD,aAAa;AAAA,EACb,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,SACE;AAAA,EACF,QAAQ;AAAA,IACN,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,EACX;AAAA,EACA,MAAM;AAAA,IACJ,SACE;AAAA,IACF,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA,EACA,WACE;AAAA,EACF,aAAa;AAAA,IACX;AAAA,IACA;AAAA,EACF;AACF,CAAC;AAgBD,IAAM,oBAAoB;AAAA,EACxB,WAAW;AAAA,IACT,aAAa;AAAA,IACb,MAAM;AAAA,EACR;AACF;AAIA,IAAM,kBAAkB,wBAAwB;AAAA,EAC9C,iBAAiB;AAAA,EACjB,aAAa;AAAA,EACb,WAAW;AACb,CAAC;AAED,IAAM,cAAc,CAAC,UAAU,YAAY,aAAa;AAMxD,IAAM,qBAAqB,uBAAuB,WAAW;AA6D7D,SAAS,gBACP,QACA,KACmB;AACnB,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AACA,aAAW,QAAQ,OAAO,MAAM,GAAG,GAAG;AACpC,UAAM,IAAI,KAAK,MAAM,uBAAuB;AAC5C,QAAI,CAAC,GAAG;AACN;AAAA,IACF;AACA,UAAM,MAAM,EAAE,CAAC;AACf,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,WAAW,MAAM,MAAM,eAAe;AAC5C,QAAI,CAAC,YAAY,SAAS,CAAC,MAAM,KAAK;AACpC;AAAA,IACF;AACA,UAAM,eAAe,MAAM,MAAM,mBAAmB;AACpD,UAAM,aAAa,eAAe,aAAa,CAAC,MAAM,SAAS;AAC/D,WAAO,EAAE,KAAK,WAAW;AAAA,EAC3B;AACA,SAAO;AACT;AAEA,IAAM,kBAAkB;AACxB,IAAM,kBAAkB,WAAW,eAAe;AAClD,IAAM,2BAA2B;AACjC,IAAM,+BAA+B;AACrC,IAAM,gBAAgB;AACtB,IAAM,mBAAmB;AACzB,IAAM,qBAAqB;AAC3B,IAAM,0BAA0B;AAChC,IAAM,kBAAkB;AAExB,SAAS,cACP,WACA,UACQ;AACR,QAAM,IAAI,aAAa;AACvB,MAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAAG;AAChC,WAAO;AAAA,EACT;AACA,SAAO,KAAK,IAAI,KAAK,MAAM,CAAC,GAAG,aAAa;AAC9C;AAEA,IAAM,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAEjC,IAAM,sBAAsB,EAAE;AAAA,EAC5B,EAAE,OAAO;AAAA,IACP,IAAI;AAAA,IACJ,SAAS,EAAE,OAAO;AAAA,IAClB,OAAO,EAAE,OAAO;AAAA,IAChB,OAAO,EAAE,KAAK,CAAC,SAAS,QAAQ,WAAW,SAAS,OAAO,CAAC;AAAA,IAC5D,QAAQ,EAAE,KAAK,CAAC,YAAY,cAAc,SAAS,CAAC;AAAA,IACpD,WAAW,EAAE,IAAI,SAAS;AAAA,IAC1B,UAAU,EAAE,IAAI,SAAS;AAAA,IACzB,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;AAAA,IAC1E,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,IACxC,SAAS,EAAE,OAAO;AAAA,MAChB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACtB,IAAI,EAAE,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS;AAAA,MAC7C,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,MAC1B,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC3C,CAAC;AAAA,IACD,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,IAClC,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IAC5C,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IAC1C,SAAS,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC9B,cAAc,EAAE,QAAQ,EAAE,SAAS;AAAA,IACnC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC/B,cAAc,EAAE,QAAQ,EAAE,SAAS;AAAA,IACnC,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,IAClC,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,IACnC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC/B,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACvC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC/B,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS;AAAA,IACrD,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACzC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,kBAAkB,EAAE,IAAI,SAAS,EAAE,SAAS,EAAE,SAAS;AAAA,IACvD,0BAA0B,EAAE,IAAI,SAAS,EAAE,SAAS,EAAE,SAAS;AAAA,IAC/D,kCAAkC,EAAE,IAAI,SAAS,EAAE,SAAS,EAAE,SAAS;AAAA,IACvE,qBAAqB,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACpD,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,IACpC,qBAAqB,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IACrD,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC1C,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,CAAC;AACH;AAEA,IAAM,2BAA2B,EAAE;AAAA,EACjC,EAAE,OAAO;AAAA,IACP,IAAI,EAAE,OAAO,EAAE,SAAS;AAAA,IACxB,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,IAC7B,aAAa,EAAE,IAAI,SAAS;AAAA,IAC5B,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACxC,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IACzC,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,IAC7B,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;AAAA,IAC5C,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IAC3C,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,IAC7B,cAAc,EAAE,OAAO,EAAE,SAAS;AAAA,IAClC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IAC/B,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC3B,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,IAC3B,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC7B,CAAC;AACH;AAEA,IAAM,wBAAwB,EAAE;AAAA,EAC9B,EAAE,OAAO;AAAA,IACP,SAAS;AAAA,IACT,aAAa,EAAE,IAAI,SAAS;AAAA,IAC5B,cAAc,EAAE,IAAI,SAAS,EAAE,SAAS,EAAE,SAAS;AAAA,IACnD,WAAW,EAAE,IAAI,SAAS,EAAE,SAAS,EAAE,SAAS;AAAA,IAChD,UAAU,EAAE;AAAA,MACV,EAAE,OAAO;AAAA,QACP,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,QACtB,eAAe,EAAE,QAAQ,EAAE,SAAS;AAAA,QACpC,IAAI,EAAE,QAAQ,EAAE,SAAS;AAAA,QACzB,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,QAC3B,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,QAChC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,QAC/B,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,IACA,SAAS,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC9B,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,IAClC,oBAAoB,EAAE,QAAQ,EAAE,SAAS;AAAA,IACzC,MAAM,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC3B,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,IAClC,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IAC5C,IAAI,EAAE,QAAQ,EAAE,SAAS;AAAA,IACzB,YAAY,EAAE,QAAQ,EAAE,SAAS;AAAA,IACjC,YAAY,EAAE,QAAQ,EAAE,SAAS;AAAA,IACjC,WAAW,EAAE,QAAQ,EAAE,SAAS;AAAA,IAChC,OAAO,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC5B,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IACrC,cAAc,EAAE,QAAQ,EAAE,SAAS;AAAA,IACnC,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAAA,IAC7B,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IACrC,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS;AAAA,IAC3C,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EACpC,CAAC;AACH;AAEA,IAAM,2BAA2B,EAAE,OAAO;AAAA,EACxC,WAAW,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,EAAE,SAAS;AAAA,EAC9C,QAAQ,EAAE;AAAA,IACR,EAAE,OAAO;AAAA,MACP,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,MAC1D,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,EAAE,SAAS;AAAA,MAClD,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS;AAAA,IAC7D,CAAC;AAAA,EACH;AAAA,EACA,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,KAAK,EAAE,OAAO,EAAE,SAAS;AAC3B,CAAC;AAEM,IAAM,kBAAkB,gBAAgB;AAAA,EAC7C,cAAc;AAAA,IACZ,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,YAAY;AAAA,MACV;AAAA,QACE,OAAO;AAAA,QACP,KAAK,CAAC,IAAI;AAAA,QACV,QAAQ,CAAC,YAAY,cAAc,SAAS;AAAA,MAC9C;AAAA,MACA;AAAA,QACE,OAAO;AAAA,QACP,KAAK,CAAC,IAAI;AAAA,QACV,QAAQ,CAAC,SAAS,SAAS,WAAW,QAAQ,SAAS,QAAQ;AAAA,MACjE;AAAA,IACF;AAAA,IACA,WAAW,EAAE,QAAQ,oBAAoB;AAAA,EAC3C;AAAA,EACA,oBAAoB;AAAA,IAClB,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,OACE;AAAA,IACF,YAAY,CAAC;AAAA,IACb,WAAW,EAAE,cAAc,yBAAyB;AAAA,EACtD;AAAA,EACA,gBAAgB;AAAA,IACd,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,YAAY,CAAC;AAAA,IACb,WAAW,EAAE,UAAU,sBAAsB;AAAA,EAC/C;AAAA,EACA,wBAAwB;AAAA,IACtB,OAAO;AAAA,IACP,aACE;AAAA,IACF,UAAU;AAAA,IACV,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,MACV,EAAE,MAAM,WAAW,aAAa,6BAA6B;AAAA,IAC/D;AAAA,IACA,WAAW,EAAE,aAAa,yBAAyB;AAAA,EACrD;AACF,CAAC;AAEM,IAAM,KAAK;AAElB,SAAS,wBAAwB,QAA8C;AAC7E,MAAI,CAAC,QAAQ;AACX,WAAO,CAAC;AAAA,EACV;AACA,QAAM,QAAkB,CAAC;AACzB,aAAW,UAAU,QAAQ;AAC3B,QAAI,EAAE,WAAW,WAAW,OAAO,OAAO,MAAM;AAC9C;AAAA,IACF;AACA,QAAI,OAAO,UAAU,YAAY,OAAO,OAAO,UAAU,UAAU;AACjE,YAAM,KAAK,MAAM,OAAO,KAAK,EAAE;AAAA,IACjC,WAAW,OAAO,UAAU,WAAW,OAAO,OAAO,UAAU,UAAU;AACvE,YAAM,KAAK,SAAS,OAAO,KAAK,EAAE;AAAA,IACpC;AAAA,EACF;AACA,SAAO;AACT;AAEO,IAAM,kBAAN,MAAM,yBAAwB,cAGnC;AAAA,EACA,OAAgB,KAAK;AAAA,EAErB,OAAgB,YAAY;AAAA,EAE5B,OAAgB,UAAU,qBAAqB,eAAe;AAAA,EAE9D,OAAO,OAAO,OAAgB,KAAyC;AACrE,UAAM,SAAS,aAAa,MAAM,KAAK;AACvC,WAAO,IAAI;AAAA,MACT;AAAA,QACE,cAAc,OAAO;AAAA,QACrB,UAAU,OAAO;AAAA,QACjB,WAAW,OAAO;AAAA,QAClB,mBAAmB,OAAO;AAAA,QAC1B,oBAAoB,OAAO;AAAA,MAC7B;AAAA,MACA,EAAE,WAAW,OAAO,UAAU;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAAA,EAES,KAAK;AAAA,EACI,cAAc;AAAA,EAExB,eAAuC;AAC7C,WAAO;AAAA,MACL,eAAe,UAAU,KAAK,MAAM,SAAS;AAAA,MAC7C,cAAc,mBAAmB,QAAQ;AAAA,IAC3C;AAAA,EACF;AAAA,EAEQ,MACN,KACA,UACA,QAC0B;AAC1B,WAAO,KAAK,IAAO,KAAK;AAAA,MACtB;AAAA,MACA,SAAS,KAAK,aAAa;AAAA,MAC3B;AAAA,MACA,WAAW;AAAA,IACb,CAAC;AAAA,EACH;AAAA,EAEQ,eAA8B;AACpC,WAAO;AAAA,MACL,CAAC,MAAM;AACL,gBAAQ,GAAG;AAAA,UACT,KAAK;AAAA,UACL,KAAK;AACH,mBAAO;AAAA,UACT,KAAK;AACH,mBAAO;AAAA,UACT,KAAK;AACH,mBAAO;AAAA,QACX;AAAA,MACF;AAAA,MACA;AAAA,MACA,KAAK,SAAS;AAAA,IAChB;AAAA,EACF;AAAA,EAEQ,gBAAgB,OAAmC;AACzD,UAAM,MAAM,KAAK,SAAS;AAC1B,YAAQ,OAAO;AAAA,MACb,KAAK;AACH,eAAO,wBAAwB,GAAG;AAAA,MACpC,KAAK;AACH,eAAO,wBAAwB,GAAG;AAAA,MACpC,KAAK;AACH,eAAO;AAAA,IACX;AAAA,EACF;AAAA,EAEQ,gBACN,OACA,SACe;AACf,UAAM,cAAc,KAAK,gBAAgB,KAAK;AAC9C,QAAI,gBAAgB,MAAM;AACxB,aAAO;AAAA,IACT;AACA,WAAO,mBAAmB;AAAA,MACxB,KAAK;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AAAA,EAEQ,cAAc,QAA+C;AACnE,QAAI,CAAC,mBAAmB,MAAM,GAAG;AAC/B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,MACL,OAAO,OAAO;AAAA,MACd,MAAM,KAAK,gBAAgB,OAAO,OAAO,OAAO,IAAI;AAAA,IACtD;AAAA,EACF;AAAA,EAEQ,sBACN,SACA,MACQ;AACR,UAAM,IAAI,IAAI;AAAA,MACZ,GAAG,eAAe,kBAAkB,KAAK,SAAS,YAAY;AAAA,IAChE;AACA,MAAE,aAAa;AAAA,MACb;AAAA,MACA,OAAO,cAAc,QAAQ,UAAU,gBAAgB,CAAC;AAAA,IAC1D;AACA,MAAE,aAAa,IAAI,QAAQ,MAAM;AACjC,eAAW,WAAW,KAAK,SAAS,YAAY,CAAC,GAAG;AAClD,QAAE,aAAa,OAAO,WAAW,OAAO;AAAA,IAC1C;AACA,UAAM,aAAuB,CAAC;AAC9B,QAAI,QAAQ,OAAO;AACjB,iBAAW,KAAK,aAAa,QAAQ,KAAK,EAAE;AAAA,IAC9C;AACA,eAAW,KAAK,GAAG,wBAAwB,MAAM,MAAM,CAAC;AACxD,QAAI,WAAW,SAAS,GAAG;AACzB,QAAE,aAAa,IAAI,SAAS,WAAW,KAAK,GAAG,CAAC;AAAA,IAClD;AACA,WAAO,EAAE,SAAS;AAAA,EACpB;AAAA,EAEQ,WACN,SACA,UACuB;AACvB,UAAM,QAAQ,QAAQ,aAAa,QAAQ;AAC3C,WAAO,SAAS,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI;AAAA,EAClD;AAAA,EAEQ,wBAAwB,SAA8B;AAC5D,UAAM,IAAI,IAAI;AAAA,MACZ,GAAG,eAAe,kBAAkB,KAAK,SAAS,YAAY;AAAA,IAChE;AACA,MAAE,aAAa;AAAA,MACb;AAAA,MACA,OAAO,cAAc,QAAQ,UAAU,kBAAkB,CAAC;AAAA,IAC5D;AACA,MAAE,aAAa,IAAI,QAAQ,MAAM;AACjC,eAAW,WAAW,KAAK,SAAS,YAAY,CAAC,GAAG;AAClD,QAAE,aAAa,OAAO,WAAW,OAAO;AAAA,IAC1C;AACA,WAAO,EAAE,SAAS;AAAA,EACpB;AAAA,EAEQ,gBAAwB;AAC9B,UAAM,WACJ,KAAK,SAAS,sBAAsB;AACtC,UAAM,IAAI,IAAI;AAAA,MACZ,GAAG,eAAe,kBAAkB,KAAK,SAAS,YAAY;AAAA,IAChE;AACA,MAAE,aAAa,IAAI,SAAS,eAAe;AAC3C,MAAE,aAAa,IAAI,YAAY,OAAO;AACtC,MAAE,aAAa,IAAI,WAAW,UAAU;AACxC,MAAE,aAAa,IAAI,YAAY,IAAI;AACnC,MAAE,aAAa,IAAI,eAAe,GAAG,QAAQ,GAAG;AAChD,MAAE,aAAa,OAAO,WAAW,SAAS;AAC1C,UAAM,WAAW,KAAK,SAAS,YAAY,CAAC;AAC5C,QAAI,SAAS,SAAS,GAAG;AACvB,iBAAW,WAAW,UAAU;AAC9B,UAAE,aAAa,OAAO,WAAW,OAAO;AAAA,MAC1C;AAAA,IACF,OAAO;AACL,QAAE,aAAa,OAAO,WAAW,IAAI;AAAA,IACvC;AACA,WAAO,EAAE,SAAS;AAAA,EACpB;AAAA,EAEQ,oBAAoB,SAAyB;AACnD,UAAM,MAAM,KAAK,SAAS,qBAAqB;AAC/C,UAAM,IAAI,IAAI,IAAI,GAAG,eAAe,WAAW,OAAO,UAAU;AAChE,MAAE,aAAa;AAAA,MACb;AAAA,MACA,OAAO,KAAK,IAAI,KAAK,wBAAwB,CAAC;AAAA,IAChD;AACA,WAAO,EAAE,SAAS;AAAA,EACpB;AAAA,EAEA,MAAc,gBACZ,MACA,SACA,QAC2D;AAC3D,UAAM,MACJ,QACA,KAAK;AAAA,MACH;AAAA,MACA,KAAK,WAAW,SAAS,cAAc;AAAA,IACzC;AACF,UAAM,MAAM,MAAM,KAAK,MAAqB,KAAK,UAAU,MAAM;AAEjE,UAAM,WAAW,gBAAgB,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM;AAChE,UAAM,OACJ,YAAY,SAAS,aACjB,KAAK,gBAAgB,UAAU,SAAS,GAAG,IAC3C;AAEN,UAAM,gBAAgB,oBAAI,IAAgC;AAC1D,QAAI,KAAK,kBAAkB,cAAc,GAAG;AAC1C,cAAQ,eAAe;AACvB,YAAM,UAAU,MAAM;AAAA,QACpB,IAAI;AAAA,QACJ;AAAA,QACA,OAAO,UAAU;AACf,gBAAM,YAAY,MAAM,KAAK;AAAA,YAC3B,KAAK,oBAAoB,MAAM,EAAE;AAAA,YACjC;AAAA,YACA;AAAA,UACF;AACA,iBAAO,CAAC,MAAM,IAAI,UAAU,IAAI;AAAA,QAClC;AAAA,MACF;AACA,iBAAW,CAAC,SAAS,MAAM,KAAK,SAAS;AACvC,sBAAc,IAAI,SAAS,MAAM;AAAA,MACnC;AAAA,IACF;AAEA,WAAO,EAAE,OAAO,CAAC,EAAE,QAAQ,IAAI,MAAM,cAAc,CAAC,GAAG,KAAK;AAAA,EAC9D;AAAA,EAEA,MAAc,kBACZ,MACA,SACA,QAC0D;AAC1D,UAAM,MAAM,QAAQ,KAAK,wBAAwB,OAAO;AACxD,UAAM,MAAM,MAAM,KAAK,MAAuB,KAAK,YAAY,MAAM;AACrE,UAAM,WAAW,gBAAgB,IAAI,QAAQ,IAAI,MAAM,GAAG,MAAM;AAChE,UAAM,WAAW,IAAI;AACrB,UAAM,SAAS,QAAQ,QAAQ,IAAI,KAAK,QAAQ,KAAK,EAAE,QAAQ,IAAI;AACnE,UAAM,WACJ,WAAW,OACP,SAAS,OAAO,CAAC,MAAM;AACrB,YAAM,KAAK,IAAI,KAAK,EAAE,WAAW,EAAE,QAAQ;AAC3C,aAAO,OAAO,SAAS,EAAE,IAAI,MAAM,SAAS;AAAA,IAC9C,CAAC,IACD;AACN,UAAM,cAAc,SAAS,GAAG,EAAE;AAClC,UAAM,SAAS,cACX,IAAI,KAAK,YAAY,WAAW,EAAE,QAAQ,IAC1C;AACJ,UAAM,gBACJ,WAAW,QACX,WAAW,QACX,OAAO,SAAS,MAAM,KACtB,SAAS;AACX,UAAM,OACJ,CAAC,iBAAiB,YAAY,SAAS,aACnC,KAAK,gBAAgB,YAAY,SAAS,GAAG,IAC7C;AACN,WAAO,EAAE,OAAO,UAAU,KAAK;AAAA,EACjC;AAAA,EAEA,MAAc,gBACZ,QACgE;AAChE,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,KAAK,cAAc;AAAA,MACnB;AAAA,MACA;AAAA,IACF;AACA,WAAO,EAAE,OAAO,CAAC,IAAI,IAAI,GAAG,MAAM,KAAK;AAAA,EACzC;AAAA,EAEA,MAAc,gBACZ,SACA,MACe;AACf,UAAM,gBAAgB,KAAK,kBAAkB,QAAQ;AACrD,UAAM,cAAc,KAAK,kBAAkB,cAAc;AAEzD,eAAW,SAAS,KAAK,QAAQ;AAC/B,UAAI,eAAe;AACjB,cAAM,QACJ,OAAO,MAAM,UAAU,WAAW,OAAO,MAAM,KAAK,IAAI,MAAM;AAChE,cAAM,cAAc,WAAW,MAAM,WAAW,KAAK;AACrD,cAAM,aAAa,WAAW,MAAM,UAAU,KAAK;AACnD,YAAI,gBAAgB,QAAQ,eAAe,MAAM;AAC/C,kBAAQ;AAAA,YACN,qCAAqC,MAAM,EAAE;AAAA,UAC/C;AAAA,QACF,OAAO;AACL,gBAAM,QAAQ,OAAO;AAAA,YACnB,MAAM;AAAA,YACN,IAAI,MAAM;AAAA,YACV,YAAY;AAAA,cACV,SAAS,MAAM;AAAA,cACf,OAAO,MAAM;AAAA,cACb,OAAO,MAAM;AAAA,cACb,QAAQ,MAAM;AAAA,cACd,WAAW;AAAA,cACX,UAAU;AAAA,cACV,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ;AAAA,cACxC,WAAW,MAAM;AAAA,cACjB,aAAa,MAAM,QAAQ;AAAA,YAC7B;AAAA,YACA,YAAY;AAAA,UACd,CAAC;AAAA,QACH;AAAA,MACF;AAEA,UAAI,aAAa;AACf,cAAM,SAAS,KAAK,cAAc,IAAI,MAAM,EAAE,KAAK,CAAC;AACpD,mBAAW,MAAM,QAAQ;AACvB,gBAAM,UAAU,GAAG,WAAW,GAAG,MAAM;AACvC,cAAI,YAAY,MAAM;AACpB;AAAA,UACF;AACA,gBAAM,UAAU,WAAW,GAAG,aAAa,KAAK;AAChD,cAAI,YAAY,MAAM;AACpB;AAAA,UACF;AACA,gBAAM,QAAQ,MAAM;AAAA,YAClB,MAAM;AAAA,YACN,UAAU;AAAA,YACV,QAAQ;AAAA,YACR,YAAY;AAAA,cACV;AAAA,cACA,SAAS,MAAM;AAAA,cACf,cAAc,MAAM;AAAA,cACpB,aAAa,MAAM,QAAQ;AAAA,cAC3B,OAAO,MAAM;AAAA,cACb,UAAU,GAAG,YAAY;AAAA,cACzB,aAAa,GAAG,eAAe;AAAA,cAC/B,SAAS,GAAG,WAAW;AAAA,YACzB;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAc,cACZ,SACA,UACe;AACf,eAAW,KAAK,UAAU;AACxB,YAAM,YAAY,WAAW,EAAE,aAAa,KAAK;AACjD,YAAM,aAAa,WAAW,EAAE,cAAc,KAAK;AACnD,YAAM,cAAc,WAAW,EAAE,WAAW,KAAK;AACjD,UAAI,cAAc,MAAM;AACtB,gBAAQ;AAAA,UACN,uCAAuC,EAAE,OAAO;AAAA,QAClD;AACA;AAAA,MACF;AACA,YAAM,QAAQ,OAAO;AAAA,QACnB,MAAM;AAAA,QACN,IAAI,EAAE;AAAA,QACN,YAAY;AAAA,UACV,SAAS,EAAE;AAAA,UACX,UAAU,EAAE,SAAS,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,UACtC,aAAa;AAAA,UACb,cAAc;AAAA,UACd,WAAW;AAAA,QACb;AAAA,QACA,YAAY,KAAK,IAAI,WAAW,cAAc,GAAG,eAAe,CAAC;AAAA,MACnE,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,MAAc,gBACZ,SACA,OACe;AACf,UAAM,UAKD,CAAC;AAEN,QAAI,YAAY,MAAM,aAAa,CAAC;AACpC,QAAI,UAAU,WAAW,KAAK,MAAM,OAAO;AACzC,YAAM,YAAY,MAAM,OAAO,OAAO,CAAC,KAAK,UAAU;AACpD,cAAM,MAAM,MAAM,SAAS,eAAe,GAAG,UAAU;AACvD,eAAO,KAAK,IAAI,KAAK,GAAG;AAAA,MAC1B,GAAG,CAAC;AACJ,YAAM,UAAU,WAAW,MAAM,OAAO,KAAK;AAC7C,UAAI,YAAY,KAAK,YAAY,MAAM;AACrC,oBAAY,MAAM;AAAA,UAAK,EAAE,QAAQ,UAAU;AAAA,UAAG,CAAC,GAAG,MAChD,IAAI,KAAK,UAAU,IAAI,IAAS,EAAE,YAAY;AAAA,QAChD;AAAA,MACF;AAAA,IACF;AAEA,eAAW,SAAS,MAAM,QAAQ;AAChC,YAAM,UAAU,MAAM,GAAG,SAAS;AAClC,YAAM,aAAa,YAAY,SAAY,OAAO,OAAO,IAAI;AAC7D,YAAM,SAAS,MAAM,SAAS,eAAe,KAAK,CAAC;AACnD,eAAS,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;AACzC,cAAM,cAAc,UAAU,CAAC;AAC/B,YAAI,gBAAgB,QAAW;AAC7B;AAAA,QACF;AACA,cAAM,KAAK,WAAW,aAAa,KAAK;AACxC,YAAI,OAAO,MAAM;AACf;AAAA,QACF;AACA,cAAM,WAAW,OAAO,CAAC;AACzB,cAAM,QAAQ,aAAa,SAAY,IAAI,OAAO,QAAQ;AAC1D,YAAI,CAAC,OAAO,SAAS,KAAK,GAAG;AAC3B;AAAA,QACF;AACA,gBAAQ,KAAK;AAAA,UACX,MAAM;AAAA,UACN;AAAA,UACA;AAAA,UACA,YAAY,EAAE,SAAS,WAAW;AAAA,QACpC,CAAC;AAAA,MACH;AAAA,IACF;AACA,UAAM,QAAQ,QAAQ,SAAS,EAAE,OAAO,CAAC,wBAAwB,EAAE,CAAC;AAAA,EACtE;AAAA,EAEA,MAAM,KACJ,SACA,SACA,QACqB;AACrB,UAAM,SAAS,KAAK,cAAc,QAAQ,MAAM;AAChD,UAAM,SAAS,QAAQ,SAAS;AAChC,UAAM,SAAS,KAAK,aAAa;AAEjC,WAAO,gBAAqC;AAAA,MAC1C;AAAA,MACA;AAAA,MACA;AAAA,MACA,QAAQ,KAAK;AAAA,MACb,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,WAAW,OAAO,OAAO,MAAM,QAAQ;AACrC,gBAAQ,OAAO;AAAA,UACb,KAAK;AACH,mBAAO,KAAK,gBAAgB,MAAM,SAAS,GAAG;AAAA,UAChD,KAAK;AACH,mBAAO,KAAK,kBAAkB,MAAM,SAAS,GAAG;AAAA,UAClD,KAAK;AACH,mBAAO,KAAK,gBAAgB,GAAG;AAAA,QACnC;AAAA,MACF;AAAA,MACA,YAAY,OAAO,OAAO,OAAO,SAAS;AACxC,YAAI,UAAU,SAAS,MAAM;AAC3B,kBAAQ,OAAO;AAAA,YACb,KAAK;AACH,kBAAI,KAAK,kBAAkB,QAAQ,GAAG;AACpC,sBAAM,QAAQ,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC;AAAA,cACxD;AACA,kBAAI,KAAK,kBAAkB,cAAc,GAAG;AAC1C,sBAAM,QAAQ,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,oBAAoB,EAAE,CAAC;AAAA,cAC5D;AACA;AAAA,YACF,KAAK;AACH,oBAAM,QAAQ,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,gBAAgB,EAAE,CAAC;AACxD;AAAA,YACF,KAAK;AACH;AAAA,UACJ;AAAA,QACF;AACA,gBAAQ,OAAO;AAAA,UACb,KAAK;AACH,uBAAW,QAAQ,OAA2B;AAC5C,oBAAM,KAAK,gBAAgB,SAAS,IAAI;AAAA,YAC1C;AACA;AAAA,UACF,KAAK;AACH,mBAAO,KAAK,cAAc,SAAS,KAAwB;AAAA,UAC7D,KAAK;AACH,uBAAW,SAAS,OAAgC;AAClD,oBAAM,KAAK,gBAAgB,SAAS,KAAK;AAAA,YAC3C;AACA;AAAA,QACJ;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;;;AC94BA,IAAO,gBAAQ;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-sentry",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Rawdash connector for Sentry — issues, issue events, releases, and error-rate metrics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "zod": "^4.4.3",
27
- "@rawdash/core": "0.24.0"
27
+ "@rawdash/core": "0.26.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "fast-check": "^4.8.0",