@rawdash/connector-sentry 0.14.0 → 0.15.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
@@ -14,32 +14,29 @@ You also need your organization **slug** (the segment in your Sentry URL, e.g. `
14
14
  ## Configuration
15
15
 
16
16
  ```ts
17
- import { SentryConnector } from '@rawdash/connector-sentry';
18
17
  import { secret } from '@rawdash/core';
19
18
 
20
- const sentry = new SentryConnector(
21
- {
19
+ const sentry = {
20
+ name: 'sentry',
21
+ connectorId: 'sentry',
22
+ config: {
22
23
  organization: 'acme',
24
+ authToken: secret('SENTRY_AUTH_TOKEN'),
23
25
  // projects: ['web', 'api'], // optional — restrict to specific project slugs or IDs
24
26
  // resources: ['issues', 'issue_events'], // optional — defaults to all four
25
27
  // eventsPerIssueCap: 100, // optional — max events sampled per issue (default 100)
26
28
  // statsLookbackHours: 24, // optional — hours of hourly stats refreshed per sync (default 24)
27
29
  },
28
- {
29
- authToken: secret('SENTRY_AUTH_TOKEN'),
30
- },
31
- );
30
+ };
32
31
  ```
33
32
 
34
- Or via `SentryConnector.create` (validates the input with the `configFields` Zod schema):
33
+ Register the connector class when mounting the engine:
35
34
 
36
35
  ```ts
37
- const sentry = SentryConnector.create({
38
- authToken: { $secret: 'SENTRY_AUTH_TOKEN' },
39
- organization: 'acme',
40
- // projects: ['web'],
41
- // resources: ['issues', 'errors_per_hour'],
42
- });
36
+ import { SentryConnector } from '@rawdash/connector-sentry';
37
+ import { mountEngine } from '@rawdash/hono';
38
+
39
+ mountEngine(config, { connectorRegistry: { sentry: SentryConnector } });
43
40
  ```
44
41
 
45
42
  ### Choosing resources
@@ -61,7 +58,7 @@ The connector exposes four resources, written across three internal sync phases:
61
58
  import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
62
59
 
63
60
  export default defineConfig({
64
- connectors: [{ connector: sentry }],
61
+ connectors: [sentry],
65
62
  dashboards: {
66
63
  errors: defineDashboard({
67
64
  widgets: {
@@ -117,6 +114,17 @@ export default defineConfig({
117
114
 
118
115
  Timestamps are stored as Unix epoch milliseconds. `sentry_issue_event` rows are sampled — the connector fetches at most `eventsPerIssueCap` recent events per issue per sync (Sentry caps a single `/events` page at 100), so the events shape is a representative sample, not a full audit trail.
119
116
 
117
+ ## Schemas
118
+
119
+ `SentryConnector.schemas` declares the Zod schema for each resource's raw API response. Used by the cloud shape-drift pipeline to populate `connector_baselines`, and by the package's property tests.
120
+
121
+ | Resource | Represents |
122
+ | -------------- | ---------------------------------------------------------------- |
123
+ | `issues` | `GET /api/0/organizations/{org}/issues/` page |
124
+ | `issue_events` | `GET /api/0/issues/{id}/events/` page |
125
+ | `releases` | `GET /api/0/organizations/{org}/releases/` page |
126
+ | `error_stats` | `GET /api/0/organizations/{org}/stats_v2/` — hourly error counts |
127
+
120
128
  ## Sync behaviour
121
129
 
122
130
  - **Backfill** (`mode: 'full'`): paginates `/api/0/organizations/{org}/issues/` and `/releases/` via Sentry's Link header (page size 100), respecting `results="true"` to stop cleanly. Issue and release entity scopes (plus the `sentry_issue_event` event scope) are cleared at the start of their phase so deletions in Sentry converge.
package/dist/index.d.ts CHANGED
@@ -33,6 +33,60 @@ declare const sentryCredentials: {
33
33
  type SentryCredentials = typeof sentryCredentials;
34
34
  declare class SentryConnector extends BaseConnector<SentrySettings, SentryCredentials> {
35
35
  static readonly id = "sentry";
36
+ static readonly schemas: {
37
+ readonly issues: z.ZodArray<z.ZodObject<{
38
+ id: z.ZodString;
39
+ shortId: z.ZodString;
40
+ title: z.ZodString;
41
+ level: z.ZodEnum<{
42
+ error: "error";
43
+ debug: "debug";
44
+ info: "info";
45
+ warning: "warning";
46
+ fatal: "fatal";
47
+ }>;
48
+ status: z.ZodEnum<{
49
+ resolved: "resolved";
50
+ unresolved: "unresolved";
51
+ ignored: "ignored";
52
+ }>;
53
+ firstSeen: z.ZodISODateTime;
54
+ lastSeen: z.ZodISODateTime;
55
+ count: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
56
+ userCount: z.ZodNumber;
57
+ project: z.ZodObject<{
58
+ slug: z.ZodString;
59
+ }, z.core.$strip>;
60
+ }, z.core.$strip>>;
61
+ readonly issue_events: z.ZodArray<z.ZodObject<{
62
+ id: z.ZodOptional<z.ZodString>;
63
+ eventID: z.ZodOptional<z.ZodString>;
64
+ dateCreated: z.ZodISODateTime;
65
+ message: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ platform: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ groupID: z.ZodOptional<z.ZodString>;
68
+ environment: z.ZodOptional<z.ZodNullable<z.ZodString>>;
69
+ }, z.core.$strip>>;
70
+ readonly releases: z.ZodArray<z.ZodObject<{
71
+ version: z.ZodString;
72
+ dateCreated: z.ZodISODateTime;
73
+ dateReleased: z.ZodNullable<z.ZodISODateTime>;
74
+ lastEvent: z.ZodNullable<z.ZodISODateTime>;
75
+ projects: z.ZodArray<z.ZodObject<{
76
+ slug: z.ZodString;
77
+ }, z.core.$strip>>;
78
+ }, z.core.$strip>>;
79
+ readonly error_stats: z.ZodObject<{
80
+ intervals: z.ZodArray<z.ZodISODateTime>;
81
+ groups: z.ZodArray<z.ZodObject<{
82
+ by: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
83
+ totals: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
84
+ series: z.ZodRecord<z.ZodString, z.ZodArray<z.ZodNumber>>;
85
+ }, z.core.$strip>>;
86
+ start: z.ZodOptional<z.ZodString>;
87
+ end: z.ZodOptional<z.ZodString>;
88
+ }, z.core.$strip>;
89
+ };
36
90
  static create(input: unknown, ctx?: ConnectorContext): SentryConnector;
37
91
  readonly id = "sentry";
38
92
  readonly credentials: {
package/dist/index.js CHANGED
@@ -114,8 +114,61 @@ var DEFAULT_EVENTS_PER_ISSUE = 100;
114
114
  var DEFAULT_STATS_LOOKBACK_HOURS = 24;
115
115
  var ISSUES_PAGE_SIZE = 100;
116
116
  var RELEASES_PAGE_SIZE = 100;
117
+ var idString = z.string().min(1);
118
+ var issueResponseSchema = z.array(
119
+ z.object({
120
+ id: idString,
121
+ shortId: z.string(),
122
+ title: z.string(),
123
+ level: z.enum(["debug", "info", "warning", "error", "fatal"]),
124
+ status: z.enum(["resolved", "unresolved", "ignored"]),
125
+ firstSeen: z.iso.datetime(),
126
+ lastSeen: z.iso.datetime(),
127
+ count: z.union([z.string().regex(/^\d+$/), z.number().int().nonnegative()]),
128
+ userCount: z.number().int().nonnegative(),
129
+ project: z.object({ slug: z.string().min(1) })
130
+ })
131
+ );
132
+ var issueEventResponseSchema = z.array(
133
+ z.object({
134
+ id: z.string().optional(),
135
+ eventID: z.string().optional(),
136
+ dateCreated: z.iso.datetime(),
137
+ message: z.string().nullable().optional(),
138
+ platform: z.string().nullable().optional(),
139
+ groupID: z.string().optional(),
140
+ environment: z.string().nullable().optional()
141
+ })
142
+ );
143
+ var releaseResponseSchema = z.array(
144
+ z.object({
145
+ version: idString,
146
+ dateCreated: z.iso.datetime(),
147
+ dateReleased: z.iso.datetime().nullable(),
148
+ lastEvent: z.iso.datetime().nullable(),
149
+ projects: z.array(z.object({ slug: z.string().min(1) }))
150
+ })
151
+ );
152
+ var errorStatsResponseSchema = z.object({
153
+ intervals: z.array(z.iso.datetime()),
154
+ groups: z.array(
155
+ z.object({
156
+ by: z.record(z.string(), z.union([z.string(), z.number()])),
157
+ totals: z.record(z.string(), z.number()).optional(),
158
+ series: z.record(z.string(), z.array(z.number()))
159
+ })
160
+ ),
161
+ start: z.string().optional(),
162
+ end: z.string().optional()
163
+ });
117
164
  var SentryConnector = class _SentryConnector extends BaseConnector {
118
165
  static id = "sentry";
166
+ static schemas = {
167
+ issues: issueResponseSchema,
168
+ issue_events: issueEventResponseSchema,
169
+ releases: releaseResponseSchema,
170
+ error_stats: errorStatsResponseSchema
171
+ };
119
172
  static create(input, ctx) {
120
173
  const parsed = configFields.parse(input);
121
174
  return new _SentryConnector(
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/pagination.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","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 const githubRateLimit: RateLimitPolicy = {\n parse(h) {\n const remainingRaw = h.get('x-ratelimit-remaining');\n const resetRaw = h.get('x-ratelimit-reset');\n if (remainingRaw === null || resetRaw === null) {\n return null;\n }\n const remaining = Number(remainingRaw);\n const reset = Number(resetRaw);\n if (!Number.isFinite(remaining) || !Number.isFinite(reset) || reset < 0) {\n return null;\n }\n return { remaining, resetAt: new Date(reset * 1000) };\n },\n};\n\nexport const sentryRateLimit: RateLimitPolicy = {\n parse(h) {\n const concurrent = h.get('x-sentry-rate-limit-remaining');\n const reset = h.get('x-sentry-rate-limit-reset');\n if (concurrent === null || reset === null) {\n return null;\n }\n const remaining = Number(concurrent);\n const resetSec = Number(reset);\n if (\n !Number.isFinite(remaining) ||\n !Number.isFinite(resetSec) ||\n resetSec < 0\n ) {\n return null;\n }\n return { remaining, resetAt: new Date(resetSec * 1000) };\n },\n};\n\nexport const linearRateLimit: RateLimitPolicy = {\n parse(h) {\n const remainingRaw = h.get('x-ratelimit-requests-remaining');\n const resetRaw = h.get('x-ratelimit-requests-reset');\n if (remainingRaw === null) {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n let resetAt: Date;\n if (resetRaw !== null) {\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n resetAt = new Date(reset);\n } else {\n resetAt = new Date(Date.now() + 60_000);\n }\n return { remaining, resetAt };\n },\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","import { type HttpResponse, sentryRateLimit } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type CredentialsSchema,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n paginateChunked,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// configFields\n// ---------------------------------------------------------------------------\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 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\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\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\nfunction isSentrySyncCursor(value: unknown): value is SentrySyncCursor {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const v = value as { phase?: unknown; page?: unknown };\n if (typeof v.phase !== 'string') {\n return false;\n }\n if (!(PHASE_ORDER as readonly string[]).includes(v.phase)) {\n return false;\n }\n if (v.page !== null && typeof v.page !== 'string') {\n return false;\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Sentry API types\n// ---------------------------------------------------------------------------\n\ninterface SentryProjectRef {\n id?: string | number;\n slug: string;\n name?: 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\n// ---------------------------------------------------------------------------\n// Link header parsing — Sentry uses Web Linking RFC 5988 plus `results=\"...\"`\n// to indicate whether a given direction has more pages. parseLinkHeader from\n// connector-shared captures the URL but not the `results` flag, so we parse\n// the raw header here.\n// ---------------------------------------------------------------------------\n\ninterface SentryLink {\n url: string;\n hasResults: boolean;\n}\n\nfunction safeTimestamp(iso: string | null | undefined): number | null {\n if (iso === null || iso === undefined) {\n return null;\n }\n const ms = new Date(iso).getTime();\n return Number.isFinite(ms) ? ms : null;\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\n// ---------------------------------------------------------------------------\n// SentryConnector\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 ISSUES_PAGE_SIZE = 100;\nconst RELEASES_PAGE_SIZE = 100;\n\nexport class SentryConnector extends BaseConnector<\n SentrySettings,\n SentryCredentials\n> {\n static readonly id = 'sentry';\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 = 'sentry';\n override readonly credentials = sentryCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.authToken}`,\n 'User-Agent': 'rawdash/connector-sentry (+https://rawdash.dev)',\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 // -------------------------------------------------------------------------\n // Resource enablement\n // -------------------------------------------------------------------------\n\n private isResourceEnabled(resource: SentryResource): boolean {\n const enabled = this.settings.resources;\n if (!enabled || enabled.length === 0) {\n return true;\n }\n return enabled.includes(resource);\n }\n\n private activePhases(): SentryPhase[] {\n const phases: SentryPhase[] = [];\n if (\n this.isResourceEnabled('issues') ||\n this.isResourceEnabled('issue_events')\n ) {\n phases.push('issues');\n }\n if (this.isResourceEnabled('releases')) {\n phases.push('releases');\n }\n if (this.isResourceEnabled('errors_per_hour')) {\n phases.push('error_stats');\n }\n return phases;\n }\n\n // -------------------------------------------------------------------------\n // URL building + sanitization\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 if (pageUrl === null) {\n return null;\n }\n const allowedPath = this.allowedPagePath(phase);\n if (allowedPath === null) {\n return null;\n }\n try {\n const u = new URL(pageUrl);\n if (\n u.protocol !== 'https:' ||\n u.host !== SENTRY_API_HOST ||\n u.pathname !== allowedPath\n ) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\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(options: SyncOptions): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/issues/`,\n );\n u.searchParams.set('limit', String(ISSUES_PAGE_SIZE));\n u.searchParams.set('sort', 'date');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n if (options.mode === 'latest' && options.since) {\n u.searchParams.set('query', `lastSeen:>${options.since}`);\n }\n return u.toString();\n }\n\n private buildInitialReleasesUrl(): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/releases/`,\n );\n u.searchParams.set('per_page', String(RELEASES_PAGE_SIZE));\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 // -------------------------------------------------------------------------\n // Fetchers\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 = page ?? this.buildInitialIssuesUrl(options);\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 for (const issue of res.body) {\n signal?.throwIfAborted();\n const eventsRes = await this.fetch<SentryIssueEvent[]>(\n this.buildIssueEventsUrl(issue.id),\n 'issue_events',\n signal,\n );\n eventsByIssue.set(issue.id, eventsRes.body);\n }\n }\n\n return { items: [{ issues: res.body, eventsByIssue }], next };\n }\n\n private async fetchReleasesPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryRelease[]; next: string | null }> {\n const url = page ?? this.buildInitialReleasesUrl();\n const res = await this.fetch<SentryRelease[]>(url, 'releases', signal);\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const next =\n nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('releases', nextLink.url)\n : null;\n return { items: res.body, 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 // -------------------------------------------------------------------------\n // Writers\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 = safeTimestamp(issue.firstSeen);\n const lastSeenMs = safeTimestamp(issue.lastSeen);\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 = safeTimestamp(ev.dateCreated);\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 = safeTimestamp(r.dateCreated);\n const releasedMs = safeTimestamp(r.dateReleased);\n const lastEventMs = safeTimestamp(r.lastEvent);\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 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 < stats.intervals.length; i++) {\n const intervalIso = stats.intervals[i];\n const rawValue = series[i];\n if (intervalIso === undefined || rawValue === undefined) {\n continue;\n }\n const ts = new Date(intervalIso).getTime();\n const value = Number(rawValue);\n if (!Number.isFinite(ts) || !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 // -------------------------------------------------------------------------\n // sync\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 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, 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 { configFields, SentryConnector } 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/pagination.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","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 const githubRateLimit: RateLimitPolicy = {\n parse(h) {\n const remainingRaw = h.get('x-ratelimit-remaining');\n const resetRaw = h.get('x-ratelimit-reset');\n if (remainingRaw === null || resetRaw === null) {\n return null;\n }\n const remaining = Number(remainingRaw);\n const reset = Number(resetRaw);\n if (!Number.isFinite(remaining) || !Number.isFinite(reset) || reset < 0) {\n return null;\n }\n return { remaining, resetAt: new Date(reset * 1000) };\n },\n};\n\nexport const sentryRateLimit: RateLimitPolicy = {\n parse(h) {\n const concurrent = h.get('x-sentry-rate-limit-remaining');\n const reset = h.get('x-sentry-rate-limit-reset');\n if (concurrent === null || reset === null) {\n return null;\n }\n const remaining = Number(concurrent);\n const resetSec = Number(reset);\n if (\n !Number.isFinite(remaining) ||\n !Number.isFinite(resetSec) ||\n resetSec < 0\n ) {\n return null;\n }\n return { remaining, resetAt: new Date(resetSec * 1000) };\n },\n};\n\nexport const linearRateLimit: RateLimitPolicy = {\n parse(h) {\n const remainingRaw = h.get('x-ratelimit-requests-remaining');\n const resetRaw = h.get('x-ratelimit-requests-reset');\n if (remainingRaw === null) {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n let resetAt: Date;\n if (resetRaw !== null) {\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n resetAt = new Date(reset);\n } else {\n resetAt = new Date(Date.now() + 60_000);\n }\n return { remaining, resetAt };\n },\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","import { type HttpResponse, sentryRateLimit } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type CredentialsSchema,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n paginateChunked,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// configFields\n// ---------------------------------------------------------------------------\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 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\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\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\nfunction isSentrySyncCursor(value: unknown): value is SentrySyncCursor {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n const v = value as { phase?: unknown; page?: unknown };\n if (typeof v.phase !== 'string') {\n return false;\n }\n if (!(PHASE_ORDER as readonly string[]).includes(v.phase)) {\n return false;\n }\n if (v.page !== null && typeof v.page !== 'string') {\n return false;\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Sentry API types\n// ---------------------------------------------------------------------------\n\ninterface SentryProjectRef {\n id?: string | number;\n slug: string;\n name?: 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\n// ---------------------------------------------------------------------------\n// Link header parsing — Sentry uses Web Linking RFC 5988 plus `results=\"...\"`\n// to indicate whether a given direction has more pages. parseLinkHeader from\n// connector-shared captures the URL but not the `results` flag, so we parse\n// the raw header here.\n// ---------------------------------------------------------------------------\n\ninterface SentryLink {\n url: string;\n hasResults: boolean;\n}\n\nfunction safeTimestamp(iso: string | null | undefined): number | null {\n if (iso === null || iso === undefined) {\n return null;\n }\n const ms = new Date(iso).getTime();\n return Number.isFinite(ms) ? ms : null;\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\n// ---------------------------------------------------------------------------\n// SentryConnector\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 ISSUES_PAGE_SIZE = 100;\nconst RELEASES_PAGE_SIZE = 100;\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\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({ slug: z.string().min(1) }),\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 }),\n);\n\nconst releaseResponseSchema = z.array(\n z.object({\n version: idString,\n dateCreated: z.iso.datetime(),\n dateReleased: z.iso.datetime().nullable(),\n lastEvent: z.iso.datetime().nullable(),\n projects: z.array(z.object({ slug: z.string().min(1) })),\n }),\n);\n\nconst errorStatsResponseSchema = z.object({\n intervals: z.array(z.iso.datetime()),\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())),\n }),\n ),\n start: z.string().optional(),\n end: z.string().optional(),\n});\n\nexport class SentryConnector extends BaseConnector<\n SentrySettings,\n SentryCredentials\n> {\n static readonly id = 'sentry';\n\n static readonly schemas = {\n issues: issueResponseSchema,\n issue_events: issueEventResponseSchema,\n releases: releaseResponseSchema,\n error_stats: errorStatsResponseSchema,\n } as const;\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 = 'sentry';\n override readonly credentials = sentryCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.authToken}`,\n 'User-Agent': 'rawdash/connector-sentry (+https://rawdash.dev)',\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 // -------------------------------------------------------------------------\n // Resource enablement\n // -------------------------------------------------------------------------\n\n private isResourceEnabled(resource: SentryResource): boolean {\n const enabled = this.settings.resources;\n if (!enabled || enabled.length === 0) {\n return true;\n }\n return enabled.includes(resource);\n }\n\n private activePhases(): SentryPhase[] {\n const phases: SentryPhase[] = [];\n if (\n this.isResourceEnabled('issues') ||\n this.isResourceEnabled('issue_events')\n ) {\n phases.push('issues');\n }\n if (this.isResourceEnabled('releases')) {\n phases.push('releases');\n }\n if (this.isResourceEnabled('errors_per_hour')) {\n phases.push('error_stats');\n }\n return phases;\n }\n\n // -------------------------------------------------------------------------\n // URL building + sanitization\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 if (pageUrl === null) {\n return null;\n }\n const allowedPath = this.allowedPagePath(phase);\n if (allowedPath === null) {\n return null;\n }\n try {\n const u = new URL(pageUrl);\n if (\n u.protocol !== 'https:' ||\n u.host !== SENTRY_API_HOST ||\n u.pathname !== allowedPath\n ) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\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(options: SyncOptions): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/issues/`,\n );\n u.searchParams.set('limit', String(ISSUES_PAGE_SIZE));\n u.searchParams.set('sort', 'date');\n for (const project of this.settings.projects ?? []) {\n u.searchParams.append('project', project);\n }\n if (options.mode === 'latest' && options.since) {\n u.searchParams.set('query', `lastSeen:>${options.since}`);\n }\n return u.toString();\n }\n\n private buildInitialReleasesUrl(): string {\n const u = new URL(\n `${SENTRY_API_BASE}/organizations/${this.settings.organization}/releases/`,\n );\n u.searchParams.set('per_page', String(RELEASES_PAGE_SIZE));\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 // -------------------------------------------------------------------------\n // Fetchers\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 = page ?? this.buildInitialIssuesUrl(options);\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 for (const issue of res.body) {\n signal?.throwIfAborted();\n const eventsRes = await this.fetch<SentryIssueEvent[]>(\n this.buildIssueEventsUrl(issue.id),\n 'issue_events',\n signal,\n );\n eventsByIssue.set(issue.id, eventsRes.body);\n }\n }\n\n return { items: [{ issues: res.body, eventsByIssue }], next };\n }\n\n private async fetchReleasesPage(\n page: string | null,\n signal: AbortSignal | undefined,\n ): Promise<{ items: SentryRelease[]; next: string | null }> {\n const url = page ?? this.buildInitialReleasesUrl();\n const res = await this.fetch<SentryRelease[]>(url, 'releases', signal);\n const nextLink = parseSentryLink(res.headers.get('link'), 'next');\n const next =\n nextLink && nextLink.hasResults\n ? this.sanitizePageUrl('releases', nextLink.url)\n : null;\n return { items: res.body, 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 // -------------------------------------------------------------------------\n // Writers\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 = safeTimestamp(issue.firstSeen);\n const lastSeenMs = safeTimestamp(issue.lastSeen);\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 = safeTimestamp(ev.dateCreated);\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 = safeTimestamp(r.dateCreated);\n const releasedMs = safeTimestamp(r.dateReleased);\n const lastEventMs = safeTimestamp(r.lastEvent);\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 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 < stats.intervals.length; i++) {\n const intervalIso = stats.intervals[i];\n const rawValue = series[i];\n if (intervalIso === undefined || rawValue === undefined) {\n continue;\n }\n const ts = new Date(intervalIso).getTime();\n const value = Number(rawValue);\n if (!Number.isFinite(ts) || !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 // -------------------------------------------------------------------------\n // sync\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 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, 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 { configFields, SentryConnector } from './sentry';\nexport type { SentryResource, SentrySettings } from './sentry';\nexport default SentryConnector;\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-sentry",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "zod": "^4.4.3",
26
- "@rawdash/core": "0.14.0"
26
+ "@rawdash/core": "0.15.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "fast-check": "^4.8.0",
@@ -31,7 +31,7 @@
31
31
  "typescript": "^5.7.2",
32
32
  "vitest": "^4.1.4",
33
33
  "@rawdash/connector-shared": "0.2.0",
34
- "@rawdash/connector-test-utils": "0.0.1"
34
+ "@rawdash/connector-test-utils": "0.0.2"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "tsup",