@rawdash/connector-firebase-crashlytics 0.21.1 → 0.22.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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-shared/src/config.ts","../../../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/firebase-crashlytics.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n","import { z } from 'zod';\n\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\n}\n","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 buildServiceAccountJwt,\n gcpAuthConfigShape,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport {\n AuthError,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type Entity,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nconst BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nconst BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport const configFields = defineConfigFields(\n z.object({\n ...gcpAuthConfigShape,\n projectId: z\n .string()\n .regex(BQ_IDENT_RE, 'projectId must be a valid GCP project id')\n .meta({\n label: 'GCP project ID',\n description:\n 'Project that hosts the Firebase Crashlytics -> BigQuery export (also the project used to bill the BigQuery queries this connector runs).',\n placeholder: 'my-firebase-project',\n }),\n bqDataset: z\n .string()\n .regex(\n BQ_DATASET_RE,\n 'bqDataset must be a valid BigQuery dataset id (letters, digits, and underscores; must start with a letter or underscore)',\n )\n .optional()\n .meta({\n label: 'BigQuery dataset',\n description:\n 'BigQuery dataset containing the Crashlytics export tables. Defaults to firebase_crashlytics (the default name Firebase uses when you enable the export).',\n placeholder: 'firebase_crashlytics',\n }),\n bqLocation: z.string().min(1).optional().meta({\n label: 'BigQuery location',\n description:\n 'Region or multi-region of the Crashlytics dataset (e.g. US, EU, us-central1). Defaults to US.',\n placeholder: 'US',\n }),\n lookbackDays: z.number().int().positive().max(720).optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to query on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n topIssuesLimit: z.number().int().positive().max(500).optional().meta({\n label: 'Top issues limit',\n description:\n 'How many top issues to retain per sync, ranked by event count over the backfill window. Defaults to 50.',\n placeholder: '50',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Firebase Crashlytics',\n category: 'engineering',\n brandColor: '#FFA000',\n tagline:\n 'Track mobile app reliability over time from the Firebase Crashlytics -> BigQuery export: daily crashes, crash-free user rate, and top issues by impact.',\n vendor: {\n name: 'Firebase',\n apiDocs: 'https://firebase.google.com/docs/crashlytics/bigquery-export',\n website: 'https://firebase.google.com/products/crashlytics',\n },\n auth: {\n summary:\n 'Authenticate against the BigQuery API with a Google service account JSON key. The service account needs the BigQuery Data Viewer role on the Crashlytics export dataset and the BigQuery Job User role on the project that runs the queries.',\n setup: [\n 'Enable the Firebase Crashlytics -> BigQuery export in the Firebase console (Project Settings -> Integrations -> BigQuery). This is a manual one-time setup per project; data starts flowing into the firebase_crashlytics dataset within a day.',\n 'Create a service account at Google Cloud -> IAM & Admin -> Service Accounts in the same project (or grant an existing one access).',\n 'Grant the service account roles/bigquery.dataViewer on the Crashlytics dataset (so it can read the export tables) and roles/bigquery.jobUser on the project (so it can run query jobs).',\n 'Generate a JSON key for the service account and store its contents as a secret (e.g. FIREBASE_SA_JSON).',\n 'Reference the key from config as serviceAccountJson: secret(\"FIREBASE_SA_JSON\") and set projectId to the Firebase project that owns the export.',\n ],\n },\n rateLimit:\n 'BigQuery jobs.query is rate-limited per project; standard 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Each connector sync runs one query per resource.',\n limitations: [\n 'Requires the Firebase Crashlytics -> BigQuery export to be configured in the Firebase console; that step is manual and one-time per project, and only days after the configuration date are present in the export.',\n 'Reads the firebase_crashlytics.<bundle>_<platform> tables via a wildcard; one row in storage covers one app/version/platform tuple per day.',\n 'Crash-free user rate is approximated from the daily ratio of unique crashing users to total event users observed in the export; matching the Firebase console number exactly requires the full Crashlytics signal, not just the BigQuery export.',\n 'Each BigQuery query is billed against the configured projectId; over long lookback windows the cost adds up. Prefer once-a-day syncs and reasonable lookbackDays.',\n 'The Crashlytics BigQuery export is streamed; the trailing 2 days are always refetched on incremental syncs to pick up late-arriving rows.',\n ],\n});\n\nconst BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nconst BQ_SCOPE = 'https://www.googleapis.com/auth/bigquery.readonly';\nconst CRASHES_METRIC_NAME = 'crashes_per_day';\nconst TOP_ISSUES_ENTITY_TYPE = 'firebase_crashlytics_issue';\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst DEFAULT_TOP_ISSUES_LIMIT = 50;\nconst DEFAULT_BQ_DATASET = 'firebase_crashlytics';\nconst INCREMENTAL_LOOKBACK_DAYS = 2;\nconst MS_PER_DAY = 86_400_000;\nconst PAGE_SIZE = 10_000;\ntype ResourceName = typeof CRASHES_METRIC_NAME | 'top_issues';\n\nexport interface FirebaseCrashlyticsSettings {\n projectId: string;\n bqDataset?: string;\n bqLocation?: string;\n lookbackDays?: number;\n topIssuesLimit?: number;\n}\n\nconst firebaseCrashlyticsCredentials = {\n serviceAccountJson: {\n description: 'Google service account JSON key (raw JSON or base64)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype FirebaseCrashlyticsCredentials = typeof firebaseCrashlyticsCredentials;\n\nconst bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\nexport const firebaseCrashlyticsResources = defineResources({\n [CRASHES_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Daily crash counts and approximate crash-free user rate per (date, application version, platform). One sample per day per app/version/platform combination present in the Crashlytics BigQuery export.',\n endpoint: 'POST /bigquery/v2/projects/{projectId}/queries',\n unit: 'crashes',\n granularity: 'daily',\n notes:\n 'Reads from firebase_crashlytics.<bundle>_<platform>_* via a wildcard. The trailing 2 days are always refetched on incremental syncs to pick up streamed rows.',\n dimensions: [\n {\n name: 'app_id',\n description:\n 'Bundle identifier (iOS) or package name (Android) of the app the crash was recorded against.',\n },\n {\n name: 'platform',\n description: 'Application platform (ios, android, or unknown).',\n },\n {\n name: 'version',\n description: 'Application display version (e.g. 2.4.1).',\n },\n {\n name: 'crash_free_user_rate',\n description:\n 'Approximate share of users on this app/version/day that did not see a crash (0..1). null if no user signal was captured.',\n },\n {\n name: 'crashing_users',\n description:\n 'Count of distinct users that experienced at least one crash on this app/version/day.',\n },\n ],\n responses: {\n oauth_token: tokenResponseSchema,\n crashes_per_day: bqQueryResponseSchema,\n },\n },\n top_issues: {\n shape: 'entity',\n description:\n 'Top crash issues by event count over the backfill window, ranked across all apps and versions present in the export. One entity per Crashlytics issue id.',\n endpoint: 'POST /bigquery/v2/projects/{projectId}/queries',\n notes:\n 'topIssuesLimit caps how many issues are retained per sync (default 50). Rows are sorted by descending event count over the backfill window.',\n fields: [\n {\n name: 'issue_id',\n description: 'Stable Crashlytics issue identifier.',\n },\n {\n name: 'title',\n description:\n 'Issue title (most recent value seen for this issue id within the window).',\n },\n {\n name: 'subtitle',\n description:\n 'Issue subtitle (most recent value seen for this issue id within the window).',\n },\n {\n name: 'app_id',\n description:\n 'Bundle identifier (iOS) or package name (Android) most recently seen for this issue.',\n },\n {\n name: 'platform',\n description: 'Application platform (ios, android, or unknown).',\n },\n {\n name: 'event_count',\n description:\n 'Total crash events attributed to this issue within the backfill window.',\n },\n {\n name: 'user_count',\n description:\n 'Distinct users that experienced this issue within the backfill window.',\n },\n {\n name: 'last_seen',\n description:\n 'ISO timestamp of the most recent event for this issue within the window.',\n },\n ],\n responses: {\n top_issues: bqQueryResponseSchema,\n },\n },\n});\n\nexport const id = 'firebase-crashlytics';\n\nexport class FirebaseCrashlyticsConnector extends BaseConnector<\n FirebaseCrashlyticsSettings,\n FirebaseCrashlyticsCredentials\n> {\n static readonly id = id;\n\n static readonly resources = firebaseCrashlyticsResources;\n\n static readonly schemas = schemasFromResources(firebaseCrashlyticsResources);\n\n static create(\n input: unknown,\n ctx?: ConnectorContext,\n ): FirebaseCrashlyticsConnector {\n const parsed = configFields.parse(input);\n return new FirebaseCrashlyticsConnector(\n {\n projectId: parsed.projectId,\n bqDataset: parsed.bqDataset,\n bqLocation: parsed.bqLocation,\n lookbackDays: parsed.lookbackDays,\n topIssuesLimit: parsed.topIssuesLimit,\n },\n { serviceAccountJson: parsed.serviceAccountJson },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = firebaseCrashlyticsCredentials;\n\n private cachedToken: { token: string; expiresAt: number } | null = null;\n\n private async getAccessToken(signal?: AbortSignal): Promise<string> {\n if (this.cachedToken && Date.now() < this.cachedToken.expiresAt) {\n return this.cachedToken.token;\n }\n const { serviceAccountJson } = this.creds;\n if (!serviceAccountJson) {\n throw new AuthError(`${this.id}: missing serviceAccountJson credential`);\n }\n const { url, body } = await buildServiceAccountJwt(\n serviceAccountJson,\n BQ_SCOPE,\n );\n const res = await this.post<{\n access_token: string;\n expires_in?: number;\n }>(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cachedToken = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cachedToken.token;\n }\n\n private async runQuery(\n accessToken: string,\n resource: ResourceName,\n sql: string,\n pageToken: string | undefined,\n signal?: AbortSignal,\n ): Promise<z.infer<typeof bqQueryResponseSchema>> {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n this.settings.projectId,\n )}/queries`;\n\n const body: Record<string, unknown> = {\n query: sql,\n useLegacySql: false,\n maxResults: PAGE_SIZE,\n timeoutMs: 30_000,\n };\n if (this.settings.bqLocation !== undefined) {\n body['location'] = this.settings.bqLocation;\n }\n if (pageToken !== undefined) {\n body['pageToken'] = pageToken;\n }\n\n const res = await this.post<z.infer<typeof bqQueryResponseSchema>>(url, {\n resource,\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent(this.id),\n },\n body: JSON.stringify(body),\n signal,\n });\n return res.body;\n }\n\n private isResourceActive(\n resource: ResourceName,\n options: SyncOptions,\n ): boolean {\n if (!options.resources) {\n return true;\n }\n return options.resources.has(resource);\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const dataset = this.settings.bqDataset ?? DEFAULT_BQ_DATASET;\n const window = getCrashlyticsWindow(\n options,\n this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS,\n );\n const topIssuesLimit =\n this.settings.topIssuesLimit ?? DEFAULT_TOP_ISSUES_LIMIT;\n\n if (this.isResourceActive(CRASHES_METRIC_NAME, options)) {\n if (signal?.aborted) {\n return { done: false };\n }\n const sql = buildCrashesPerDaySql({\n projectId: this.settings.projectId,\n bqDataset: dataset,\n startDate: window.startDate,\n endDate: window.endDate,\n });\n const samples = await this.collectSamples(sql, signal);\n if (signal?.aborted) {\n return { done: false };\n }\n await storage.metrics(samples, { names: [CRASHES_METRIC_NAME] });\n }\n\n if (this.isResourceActive('top_issues', options)) {\n if (signal?.aborted) {\n return { done: false };\n }\n const sql = buildTopIssuesSql({\n projectId: this.settings.projectId,\n bqDataset: dataset,\n startDate: window.startDate,\n endDate: window.endDate,\n limit: topIssuesLimit,\n });\n const entities = await this.collectIssues(sql, signal);\n if (signal?.aborted) {\n return { done: false };\n }\n await storage.entities(entities, { types: [TOP_ISSUES_ENTITY_TYPE] });\n }\n\n return { done: true };\n }\n\n private async collectSamples(\n sql: string,\n signal?: AbortSignal,\n ): Promise<MetricSample[]> {\n const samples: MetricSample[] = [];\n let pageToken: string | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (signal?.aborted) {\n break;\n }\n const accessToken = await this.getAccessToken(signal);\n let response: z.infer<typeof bqQueryResponseSchema>;\n try {\n response = await this.runQuery(\n accessToken,\n CRASHES_METRIC_NAME,\n sql,\n pageToken,\n signal,\n );\n } catch (err) {\n this.logger.warn('fetch page failed', {\n resource: CRASHES_METRIC_NAME,\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(\n `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the lookbackDays so the query finishes faster.`,\n );\n }\n const pageSamples = buildCrashesSamplesFromBqResponse(response);\n samples.push(...pageSamples);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n this.logger.info('fetched page', {\n resource: CRASHES_METRIC_NAME,\n page,\n items: pageSamples.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n this.logger.info('resource done', {\n resource: CRASHES_METRIC_NAME,\n pages: page,\n items: samples.length,\n duration_ms: Date.now() - phaseStart,\n });\n return samples;\n }\n\n private async collectIssues(\n sql: string,\n signal?: AbortSignal,\n ): Promise<Entity[]> {\n const entities: Entity[] = [];\n let pageToken: string | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (signal?.aborted) {\n break;\n }\n const accessToken = await this.getAccessToken(signal);\n let response: z.infer<typeof bqQueryResponseSchema>;\n try {\n response = await this.runQuery(\n accessToken,\n 'top_issues',\n sql,\n pageToken,\n signal,\n );\n } catch (err) {\n this.logger.warn('fetch page failed', {\n resource: 'top_issues',\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(\n `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the lookbackDays so the query finishes faster.`,\n );\n }\n const pageEntities = buildTopIssuesEntitiesFromBqResponse(response);\n entities.push(...pageEntities);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n this.logger.info('fetched page', {\n resource: 'top_issues',\n page,\n items: pageEntities.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n this.logger.info('resource done', {\n resource: 'top_issues',\n pages: page,\n items: entities.length,\n duration_ms: Date.now() - phaseStart,\n });\n return entities;\n }\n}\n\ninterface CrashlyticsWindow {\n startDate: string;\n endDate: string;\n}\n\nexport function buildCrashesPerDaySql(args: {\n projectId: string;\n bqDataset: string;\n startDate: string;\n endDate: string;\n}): string {\n const table = `\\`${args.projectId}.${args.bqDataset}.*\\``;\n return [\n 'WITH events AS (',\n ' SELECT',\n ' DATE(event_timestamp) AS date,',\n ' application.bundle_id AS app_id,',\n \" LOWER(IFNULL(application.platform, 'unknown')) AS platform,\",\n ' application.app_display_version AS version,',\n ' user.id AS user_id,',\n ' is_fatal',\n ` FROM ${table}`,\n ` WHERE DATE(event_timestamp) >= DATE('${args.startDate}')`,\n ` AND DATE(event_timestamp) < DATE('${args.endDate}')`,\n ')',\n 'SELECT',\n ' date,',\n ' app_id,',\n ' platform,',\n ' version,',\n ' COUNTIF(is_fatal) AS crashes,',\n ' COUNT(DISTINCT IF(is_fatal, user_id, NULL)) AS crashing_users,',\n ' COUNT(DISTINCT user_id) AS total_users',\n 'FROM events',\n 'GROUP BY date, app_id, platform, version',\n 'ORDER BY date',\n ].join('\\n');\n}\n\nexport function buildTopIssuesSql(args: {\n projectId: string;\n bqDataset: string;\n startDate: string;\n endDate: string;\n limit: number;\n}): string {\n const table = `\\`${args.projectId}.${args.bqDataset}.*\\``;\n return [\n 'SELECT',\n ' issue_id,',\n ' ANY_VALUE(issue_title HAVING MAX event_timestamp) AS title,',\n ' ANY_VALUE(issue_subtitle HAVING MAX event_timestamp) AS subtitle,',\n ' ANY_VALUE(application.bundle_id HAVING MAX event_timestamp) AS app_id,',\n \" LOWER(IFNULL(ANY_VALUE(application.platform HAVING MAX event_timestamp), 'unknown')) AS platform,\",\n ' COUNT(*) AS event_count,',\n ' COUNT(DISTINCT user.id) AS user_count,',\n ' MAX(event_timestamp) AS last_seen',\n `FROM ${table}`,\n `WHERE DATE(event_timestamp) >= DATE('${args.startDate}')`,\n ` AND DATE(event_timestamp) < DATE('${args.endDate}')`,\n ' AND issue_id IS NOT NULL',\n 'GROUP BY issue_id',\n 'ORDER BY event_count DESC',\n `LIMIT ${args.limit}`,\n ].join('\\n');\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nexport function getCrashlyticsWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): CrashlyticsWindow {\n const endMs = startOfUtcDay(now) + MS_PER_DAY;\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (options.since !== undefined) {\n const sinceMs = parseEpoch(options.since, 'iso');\n if (sinceMs !== null) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(\n Math.max(elapsed + INCREMENTAL_LOOKBACK_DAYS, 1),\n lookbackDays,\n );\n }\n }\n return {\n startDate: toDateStr(endMs - days * MS_PER_DAY),\n endDate: toDateStr(endMs),\n };\n}\n\nexport function buildCrashesSamplesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n): MetricSample[] {\n const schema = response.schema?.fields ?? [];\n const fieldIndex: Record<string, number> = {};\n schema.forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n\n const samples: MetricSample[] = [];\n for (const row of response.rows ?? []) {\n const dateValue = readCell(row.f, fieldIndex, 'date');\n if (dateValue === null) {\n continue;\n }\n const ts = parseBqDateOrEpoch(dateValue);\n if (ts === null) {\n continue;\n }\n const crashesRaw = readCell(row.f, fieldIndex, 'crashes');\n if (crashesRaw === null) {\n continue;\n }\n const crashes = Number.parseFloat(crashesRaw);\n if (!Number.isFinite(crashes)) {\n continue;\n }\n const crashingUsersRaw = readCell(row.f, fieldIndex, 'crashing_users');\n const totalUsersRaw = readCell(row.f, fieldIndex, 'total_users');\n const crashingUsers =\n crashingUsersRaw !== null ? Number.parseFloat(crashingUsersRaw) : NaN;\n const totalUsers =\n totalUsersRaw !== null ? Number.parseFloat(totalUsersRaw) : NaN;\n\n let crashFreeRate: number | null = null;\n if (\n Number.isFinite(totalUsers) &&\n totalUsers > 0 &&\n Number.isFinite(crashingUsers)\n ) {\n const rate = 1 - crashingUsers / totalUsers;\n crashFreeRate = Math.max(0, Math.min(1, rate));\n }\n\n const attributes: Record<string, JSONValue> = {};\n const appId = readCell(row.f, fieldIndex, 'app_id');\n const platform = readCell(row.f, fieldIndex, 'platform');\n const version = readCell(row.f, fieldIndex, 'version');\n attributes['app_id'] = appId;\n attributes['platform'] = platform;\n attributes['version'] = version;\n attributes['crash_free_user_rate'] = crashFreeRate;\n attributes['crashing_users'] = Number.isFinite(crashingUsers)\n ? crashingUsers\n : null;\n\n samples.push({\n name: CRASHES_METRIC_NAME,\n ts,\n value: crashes,\n attributes,\n });\n }\n return samples;\n}\n\nexport function buildTopIssuesEntitiesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n): Entity[] {\n const schema = response.schema?.fields ?? [];\n const fieldIndex: Record<string, number> = {};\n schema.forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n\n const entities: Entity[] = [];\n for (const row of response.rows ?? []) {\n const issueId = readCell(row.f, fieldIndex, 'issue_id');\n if (issueId === null || issueId.length === 0) {\n continue;\n }\n const eventCountRaw = readCell(row.f, fieldIndex, 'event_count');\n const userCountRaw = readCell(row.f, fieldIndex, 'user_count');\n const eventCount =\n eventCountRaw !== null ? Number.parseFloat(eventCountRaw) : NaN;\n const userCount =\n userCountRaw !== null ? Number.parseFloat(userCountRaw) : NaN;\n const lastSeenRaw = readCell(row.f, fieldIndex, 'last_seen');\n const lastSeenMs =\n lastSeenRaw !== null ? parseBqDateOrEpoch(lastSeenRaw) : null;\n const updatedAt = lastSeenMs ?? Date.now();\n\n const attributes: Record<string, JSONValue> = {\n issue_id: issueId,\n title: readCell(row.f, fieldIndex, 'title'),\n subtitle: readCell(row.f, fieldIndex, 'subtitle'),\n app_id: readCell(row.f, fieldIndex, 'app_id'),\n platform: readCell(row.f, fieldIndex, 'platform'),\n event_count: Number.isFinite(eventCount) ? eventCount : 0,\n user_count: Number.isFinite(userCount) ? userCount : 0,\n last_seen:\n lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n\n entities.push({\n type: TOP_ISSUES_ENTITY_TYPE,\n id: issueId,\n attributes,\n updated_at: updatedAt,\n });\n }\n return entities;\n}\n\nfunction readCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nfunction parseBqDateOrEpoch(value: string): number | null {\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { FirebaseCrashlyticsConnector } from './firebase-crashlytics';\n\nexport {\n FirebaseCrashlyticsConnector,\n buildCrashesPerDaySql,\n buildCrashesSamplesFromBqResponse,\n buildTopIssuesEntitiesFromBqResponse,\n buildTopIssuesSql,\n configFields,\n doc,\n getCrashlyticsWindow,\n id,\n firebaseCrashlyticsResources as resources,\n} from './firebase-crashlytics';\nexport type { FirebaseCrashlyticsSettings } from './firebase-crashlytics';\nexport default FirebaseCrashlyticsConnector;\n"],"mappings":"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","names":["z","z"]}
1
+ {"version":3,"sources":["../../gcp-shared/src/auth.ts","../../gcp-shared/src/config.ts","../../../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","../../gcp-shared/src/bigquery.ts","../../gcp-shared/src/access-token.ts","../../gcp-shared/src/dates.ts","../../../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/firebase-crashlytics.ts","../src/index.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport interface ServiceAccountKey {\n client_email: string;\n private_key: string;\n token_uri?: string;\n project_id?: string;\n}\n\nconst serviceAccountKeySchema = z.object({\n client_email: z.string().min(1),\n private_key: z.string().min(1),\n token_uri: z.string().url().optional(),\n project_id: z.string().optional(),\n});\n\nexport interface TokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport const tokenResponseSchema = z.object({\n access_token: z.string().min(1),\n expires_in: z.number().int().positive().optional(),\n});\n\nfunction base64urlFromBytes(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n}\n\nfunction base64urlFromString(str: string): string {\n return base64urlFromBytes(new TextEncoder().encode(str));\n}\n\nasync function signRS256JWT(\n payload: Record<string, unknown>,\n privateKeyPem: string,\n): Promise<string> {\n const header = { alg: 'RS256', typ: 'JWT' };\n const headerB64 = base64urlFromString(JSON.stringify(header));\n const payloadB64 = base64urlFromString(JSON.stringify(payload));\n const signingInput = `${headerB64}.${payloadB64}`;\n\n const pemContent = privateKeyPem\n .replace(/-----BEGIN PRIVATE KEY-----/g, '')\n .replace(/-----END PRIVATE KEY-----/g, '')\n .replace(/\\s/g, '');\n const der = Uint8Array.from(atob(pemContent), (c) => c.charCodeAt(0));\n\n const key = await globalThis.crypto.subtle.importKey(\n 'pkcs8',\n der,\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign'],\n );\n\n const signature = await globalThis.crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(signingInput),\n );\n\n return `${signingInput}.${base64urlFromBytes(new Uint8Array(signature))}`;\n}\n\nexport function parseServiceAccountJson(value: string): ServiceAccountKey {\n const trimmed = value.trim();\n if (trimmed.startsWith('{')) {\n return serviceAccountKeySchema.parse(JSON.parse(trimmed));\n }\n const binary = atob(trimmed);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const decoded = new TextDecoder().decode(bytes);\n return serviceAccountKeySchema.parse(JSON.parse(decoded));\n}\n\nexport async function buildServiceAccountJwt(\n serviceAccountJson: string,\n scope: string,\n): Promise<{ url: string; body: string }> {\n const sa = parseServiceAccountJson(serviceAccountJson);\n const now = Math.floor(Date.now() / 1000);\n const jwt = await signRS256JWT(\n {\n iss: sa.client_email,\n scope,\n aud: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now,\n },\n sa.private_key,\n );\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion: jwt,\n }).toString();\n\n return {\n url: sa.token_uri ?? 'https://oauth2.googleapis.com/token',\n body,\n };\n}\n\nexport interface RefreshTokenCredentials {\n refreshToken: string;\n clientId: string;\n clientSecret: string;\n}\n\nexport function buildRefreshTokenGrant(credentials: RefreshTokenCredentials): {\n url: string;\n body: string;\n} {\n const body = new URLSearchParams({\n grant_type: 'refresh_token',\n refresh_token: credentials.refreshToken,\n client_id: credentials.clientId,\n client_secret: credentials.clientSecret,\n }).toString();\n\n return { url: 'https://oauth2.googleapis.com/token', body };\n}\n","import { z } from 'zod';\n\nexport const gcpAuthConfigShape = {\n serviceAccountJson: z.object({ $secret: z.string().trim().min(1) }).meta({\n label: 'Service Account JSON',\n description:\n 'Contents of the JSON key file for a Google service account with the role required by this connector. Create one at Google Cloud -> IAM & Admin -> Service Accounts and store the JSON as a secret.',\n secret: true,\n }),\n} as const;\n\nexport interface GcpAuthConfig {\n serviceAccountJson: { $secret: string };\n}\n","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 { parseEpoch } from '@rawdash/connector-shared';\nimport { z } from 'zod';\n\nexport const BQ_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nexport const BQ_DATASET_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nexport const BQ_API_BASE = 'https://bigquery.googleapis.com/bigquery/v2';\nexport const BQ_READONLY_SCOPE =\n 'https://www.googleapis.com/auth/bigquery.readonly';\nexport const BQ_PAGE_SIZE = 10_000;\nexport const BQ_QUERY_TIMEOUT_MS = 30_000;\n\nexport const bqQueryResponseSchema = z.object({\n jobComplete: z.boolean().optional(),\n schema: z\n .object({\n fields: z.array(z.object({ name: z.string(), type: z.string() })),\n })\n .optional(),\n rows: z\n .array(\n z.object({\n f: z.array(z.object({ v: z.string().nullable().optional() })),\n }),\n )\n .optional(),\n pageToken: z.string().optional(),\n jobReference: z\n .object({\n projectId: z.string(),\n jobId: z.string(),\n location: z.string().optional(),\n })\n .optional(),\n});\n\nexport type BqQueryResponse = z.infer<typeof bqQueryResponseSchema>;\nexport type BqJobReference = NonNullable<BqQueryResponse['jobReference']>;\n\nexport type BqPageRequest =\n | { method: 'POST'; url: string; body: string }\n | { method: 'GET'; url: string };\n\nexport interface BqPageLogger {\n info(message: string, meta?: Record<string, unknown>): void;\n warn(message: string, meta?: Record<string, unknown>): void;\n}\n\nexport function buildBigQueryPageRequest(opts: {\n projectId: string;\n sql: string;\n pageToken: string | undefined;\n jobReference: BqJobReference | undefined;\n location?: string;\n pageSize?: number;\n timeoutMs?: number;\n}): BqPageRequest {\n const pageSize = opts.pageSize ?? BQ_PAGE_SIZE;\n const timeoutMs = opts.timeoutMs ?? BQ_QUERY_TIMEOUT_MS;\n\n if (opts.pageToken === undefined) {\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.projectId,\n )}/queries`;\n const body: Record<string, unknown> = {\n query: opts.sql,\n useLegacySql: false,\n maxResults: pageSize,\n timeoutMs,\n };\n if (opts.location !== undefined) {\n body['location'] = opts.location;\n }\n return { method: 'POST', url, body: JSON.stringify(body) };\n }\n\n if (opts.jobReference === undefined) {\n throw new Error(\n 'cannot fetch the next page of BigQuery results without a jobReference',\n );\n }\n\n const params = new URLSearchParams({\n pageToken: opts.pageToken,\n maxResults: String(pageSize),\n timeoutMs: String(timeoutMs),\n });\n const location = opts.jobReference.location ?? opts.location;\n if (location !== undefined) {\n params.set('location', location);\n }\n const url = `${BQ_API_BASE}/projects/${encodeURIComponent(\n opts.jobReference.projectId,\n )}/queries/${encodeURIComponent(opts.jobReference.jobId)}?${params.toString()}`;\n return { method: 'GET', url };\n}\n\nexport async function collectBigQueryPages<T>(opts: {\n projectId: string;\n sql: string;\n resource: string;\n fetchPage: (\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ) => Promise<BqQueryResponse>;\n mapRows: (response: BqQueryResponse) => T[];\n jobIncompleteMessage: string;\n location?: string;\n pageSize?: number;\n signal?: AbortSignal;\n logger?: BqPageLogger;\n}): Promise<{ rows: T[]; aborted: boolean }> {\n const rows: T[] = [];\n let pageToken: string | undefined;\n let jobReference: BqJobReference | undefined;\n let page = 0;\n const phaseStart = Date.now();\n\n do {\n if (opts.signal?.aborted) {\n return { rows, aborted: true };\n }\n const request = buildBigQueryPageRequest({\n projectId: opts.projectId,\n sql: opts.sql,\n pageToken,\n jobReference,\n location: opts.location,\n pageSize: opts.pageSize,\n });\n let response: BqQueryResponse;\n try {\n response = await opts.fetchPage(request, opts.signal);\n } catch (err) {\n opts.logger?.warn('fetch page failed', {\n resource: opts.resource,\n page: page + 1,\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n if (response.jobComplete === false) {\n throw new Error(opts.jobIncompleteMessage);\n }\n if (response.jobReference !== undefined) {\n jobReference = response.jobReference;\n }\n const pageRows = opts.mapRows(response);\n rows.push(...pageRows);\n pageToken =\n typeof response.pageToken === 'string' && response.pageToken.length > 0\n ? response.pageToken\n : undefined;\n page += 1;\n opts.logger?.info('fetched page', {\n resource: opts.resource,\n page,\n items: pageRows.length,\n next: pageToken ?? null,\n });\n } while (pageToken !== undefined);\n\n opts.logger?.info('resource done', {\n resource: opts.resource,\n pages: page,\n items: rows.length,\n duration_ms: Date.now() - phaseStart,\n });\n return { rows, aborted: false };\n}\n\nexport function indexBqFields(\n response: BqQueryResponse,\n): Record<string, number> {\n const fieldIndex: Record<string, number> = {};\n (response.schema?.fields ?? []).forEach((field, idx) => {\n fieldIndex[field.name] = idx;\n });\n return fieldIndex;\n}\n\nexport function readBqCell(\n cells: ReadonlyArray<{ v?: string | null }>,\n fieldIndex: Record<string, number>,\n name: string,\n): string | null {\n const idx = fieldIndex[name];\n if (idx === undefined) {\n return null;\n }\n const raw = cells[idx]?.v;\n if (raw === undefined || raw === null) {\n return null;\n }\n return raw;\n}\n\nexport function parseBqDateOrEpoch(value: string): number | null {\n const dateMatch = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(value);\n if (dateMatch) {\n return Date.UTC(\n Number(dateMatch[1]),\n Number(dateMatch[2]) - 1,\n Number(dateMatch[3]),\n );\n }\n return parseEpoch(value, 'iso');\n}\n","import { AuthError } from '@rawdash/connector-shared';\n\nimport {\n type RefreshTokenCredentials,\n buildRefreshTokenGrant,\n buildServiceAccountJwt,\n} from './auth';\n\ninterface GcpTokenResponse {\n access_token: string;\n expires_in?: number;\n}\n\nexport type GcpTokenPoster = (\n url: string,\n opts: {\n resource: string;\n headers: Record<string, string>;\n body: string;\n signal?: AbortSignal;\n },\n) => Promise<{ body: GcpTokenResponse }>;\n\nexport class GcpAccessTokenProvider {\n private cached: { token: string; expiresAt: number } | null = null;\n\n constructor(\n private readonly opts: {\n connectorId: string;\n scope: string;\n getServiceAccountJson: () => string | undefined;\n getRefreshTokenCredentials?: () => RefreshTokenCredentials | undefined;\n post: GcpTokenPoster;\n },\n ) {}\n\n private async resolveGrant(): Promise<{ url: string; body: string }> {\n const serviceAccountJson = this.opts.getServiceAccountJson();\n if (serviceAccountJson) {\n return buildServiceAccountJwt(serviceAccountJson, this.opts.scope);\n }\n const refreshTokenCredentials = this.opts.getRefreshTokenCredentials?.();\n if (refreshTokenCredentials) {\n return buildRefreshTokenGrant(refreshTokenCredentials);\n }\n throw new AuthError(\n `${this.opts.connectorId}: missing serviceAccountJson or refresh-token credentials`,\n );\n }\n\n async getToken(signal?: AbortSignal): Promise<string> {\n if (this.cached && Date.now() < this.cached.expiresAt) {\n return this.cached.token;\n }\n const { url, body } = await this.resolveGrant();\n const res = await this.opts.post(url, {\n resource: 'oauth_token',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body,\n signal,\n });\n const expiresIn = res.body.expires_in ?? 3600;\n this.cached = {\n token: res.body.access_token,\n expiresAt: Date.now() + (expiresIn - 60) * 1000,\n };\n return this.cached.token;\n }\n}\n","export const MS_PER_DAY = 86_400_000;\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nexport function toDateStr(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nexport function startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n","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 BQ_DATASET_RE,\n BQ_IDENT_RE,\n BQ_READONLY_SCOPE,\n type BqPageRequest,\n type BqQueryResponse,\n GcpAccessTokenProvider,\n MS_PER_DAY,\n bqQueryResponseSchema,\n collectBigQueryPages,\n gcpAuthConfigShape,\n indexBqFields,\n parseBqDateOrEpoch,\n readBqCell as readCell,\n startOfUtcDay,\n toDateStr,\n tokenResponseSchema,\n} from '@rawdash/connector-gcp-shared';\nimport { connectorUserAgent, parseEpoch } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type Entity,\n type JSONValue,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n schemasFromResources,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n ...gcpAuthConfigShape,\n projectId: z\n .string()\n .regex(BQ_IDENT_RE, 'projectId must be a valid GCP project id')\n .meta({\n label: 'GCP project ID',\n description:\n 'Project that hosts the Firebase Crashlytics -> BigQuery export (also the project used to bill the BigQuery queries this connector runs).',\n placeholder: 'my-firebase-project',\n }),\n bqDataset: z\n .string()\n .regex(\n BQ_DATASET_RE,\n 'bqDataset must be a valid BigQuery dataset id (letters, digits, and underscores; must start with a letter or underscore)',\n )\n .optional()\n .meta({\n label: 'BigQuery dataset',\n description:\n 'BigQuery dataset containing the Crashlytics export tables. Defaults to firebase_crashlytics (the default name Firebase uses when you enable the export).',\n placeholder: 'firebase_crashlytics',\n }),\n bqLocation: z.string().min(1).optional().meta({\n label: 'BigQuery location',\n description:\n 'Region or multi-region of the Crashlytics dataset (e.g. US, EU, us-central1). Defaults to US.',\n placeholder: 'US',\n }),\n lookbackDays: z.number().int().positive().max(720).optional().meta({\n label: 'Backfill window (days)',\n description:\n 'How many days of history to query on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n topIssuesLimit: z.number().int().positive().max(500).optional().meta({\n label: 'Top issues limit',\n description:\n 'How many top issues to retain per sync, ranked by event count over the backfill window. Defaults to 50.',\n placeholder: '50',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Firebase Crashlytics',\n category: 'engineering',\n brandColor: '#FFA000',\n tagline:\n 'Track mobile app reliability over time from the Firebase Crashlytics -> BigQuery export: daily crashes, crash-free user rate, and top issues by impact.',\n vendor: {\n name: 'Firebase',\n domain: 'firebase.google.com',\n apiDocs: 'https://firebase.google.com/docs/crashlytics/bigquery-export',\n website: 'https://firebase.google.com/products/crashlytics',\n },\n auth: {\n summary:\n 'Authenticate against the BigQuery API with a Google service account JSON key. The service account needs the BigQuery Data Viewer role on the Crashlytics export dataset and the BigQuery Job User role on the project that runs the queries.',\n setup: [\n 'Enable the Firebase Crashlytics -> BigQuery export in the Firebase console (Project Settings -> Integrations -> BigQuery). This is a manual one-time setup per project; data starts flowing into the firebase_crashlytics dataset within a day.',\n 'Create a service account at Google Cloud -> IAM & Admin -> Service Accounts in the same project (or grant an existing one access).',\n 'Grant the service account roles/bigquery.dataViewer on the Crashlytics dataset (so it can read the export tables) and roles/bigquery.jobUser on the project (so it can run query jobs).',\n 'Generate a JSON key for the service account and store its contents as a secret (e.g. FIREBASE_SA_JSON).',\n 'Reference the key from config as serviceAccountJson: secret(\"FIREBASE_SA_JSON\") and set projectId to the Firebase project that owns the export.',\n ],\n },\n rateLimit:\n 'BigQuery jobs.query is rate-limited per project; standard 429 / RESOURCE_EXHAUSTED responses are retried with backoff. Each connector sync runs one query per resource.',\n limitations: [\n 'Requires the Firebase Crashlytics -> BigQuery export to be configured in the Firebase console; that step is manual and one-time per project, and only days after the configuration date are present in the export.',\n 'Reads the firebase_crashlytics.<bundle>_<platform> tables via a wildcard; one row in storage covers one app/version/platform tuple per day.',\n 'Crash-free user rate is approximated from the daily ratio of unique crashing users to total event users observed in the export; matching the Firebase console number exactly requires the full Crashlytics signal, not just the BigQuery export.',\n 'Each BigQuery query is billed against the configured projectId; over long lookback windows the cost adds up. Prefer once-a-day syncs and reasonable lookbackDays.',\n 'The Crashlytics BigQuery export is streamed; the trailing 2 days are always refetched on incremental syncs to pick up late-arriving rows.',\n ],\n});\n\nconst CRASHES_METRIC_NAME = 'crashes_per_day';\nconst TOP_ISSUES_ENTITY_TYPE = 'firebase_crashlytics_issue';\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst DEFAULT_TOP_ISSUES_LIMIT = 50;\nconst DEFAULT_BQ_DATASET = 'firebase_crashlytics';\nconst INCREMENTAL_LOOKBACK_DAYS = 2;\ntype ResourceName = typeof CRASHES_METRIC_NAME | 'top_issues';\n\nexport interface FirebaseCrashlyticsSettings {\n projectId: string;\n bqDataset?: string;\n bqLocation?: string;\n lookbackDays?: number;\n topIssuesLimit?: number;\n}\n\nconst firebaseCrashlyticsCredentials = {\n serviceAccountJson: {\n description: 'Google service account JSON key (raw JSON or base64)',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype FirebaseCrashlyticsCredentials = typeof firebaseCrashlyticsCredentials;\n\nexport const firebaseCrashlyticsResources = defineResources({\n [CRASHES_METRIC_NAME]: {\n shape: 'metric',\n description:\n 'Daily crash counts and approximate crash-free user rate per (date, application version, platform). One sample per day per app/version/platform combination present in the Crashlytics BigQuery export.',\n endpoint: 'POST /bigquery/v2/projects/{projectId}/queries',\n unit: 'crashes',\n granularity: 'daily',\n notes:\n 'Reads from firebase_crashlytics.<bundle>_<platform>_* via a wildcard. The trailing 2 days are always refetched on incremental syncs to pick up streamed rows.',\n dimensions: [\n {\n name: 'app_id',\n description:\n 'Bundle identifier (iOS) or package name (Android) of the app the crash was recorded against.',\n },\n {\n name: 'platform',\n description: 'Application platform (ios, android, or unknown).',\n },\n {\n name: 'version',\n description: 'Application display version (e.g. 2.4.1).',\n },\n {\n name: 'crash_free_user_rate',\n description:\n 'Approximate share of users on this app/version/day that did not see a crash (0..1). null if no user signal was captured.',\n },\n {\n name: 'crashing_users',\n description:\n 'Count of distinct users that experienced at least one crash on this app/version/day.',\n },\n ],\n responses: {\n oauth_token: tokenResponseSchema,\n crashes_per_day: bqQueryResponseSchema,\n },\n },\n top_issues: {\n shape: 'entity',\n description:\n 'Top crash issues by event count over the backfill window, ranked across all apps and versions present in the export. One entity per Crashlytics issue id.',\n endpoint: 'POST /bigquery/v2/projects/{projectId}/queries',\n notes:\n 'topIssuesLimit caps how many issues are retained per sync (default 50). Rows are sorted by descending event count over the backfill window.',\n fields: [\n {\n name: 'issue_id',\n description: 'Stable Crashlytics issue identifier.',\n },\n {\n name: 'title',\n description:\n 'Issue title (most recent value seen for this issue id within the window).',\n },\n {\n name: 'subtitle',\n description:\n 'Issue subtitle (most recent value seen for this issue id within the window).',\n },\n {\n name: 'app_id',\n description:\n 'Bundle identifier (iOS) or package name (Android) most recently seen for this issue.',\n },\n {\n name: 'platform',\n description: 'Application platform (ios, android, or unknown).',\n },\n {\n name: 'event_count',\n description:\n 'Total crash events attributed to this issue within the backfill window.',\n },\n {\n name: 'user_count',\n description:\n 'Distinct users that experienced this issue within the backfill window.',\n },\n {\n name: 'last_seen',\n description:\n 'ISO timestamp of the most recent event for this issue within the window.',\n },\n ],\n responses: {\n top_issues: bqQueryResponseSchema,\n },\n },\n});\n\nexport const id = 'firebase-crashlytics';\n\nexport class FirebaseCrashlyticsConnector extends BaseConnector<\n FirebaseCrashlyticsSettings,\n FirebaseCrashlyticsCredentials\n> {\n static readonly id = id;\n\n static readonly resources = firebaseCrashlyticsResources;\n\n static readonly schemas = schemasFromResources(firebaseCrashlyticsResources);\n\n static create(\n input: unknown,\n ctx?: ConnectorContext,\n ): FirebaseCrashlyticsConnector {\n const parsed = configFields.parse(input);\n return new FirebaseCrashlyticsConnector(\n {\n projectId: parsed.projectId,\n bqDataset: parsed.bqDataset,\n bqLocation: parsed.bqLocation,\n lookbackDays: parsed.lookbackDays,\n topIssuesLimit: parsed.topIssuesLimit,\n },\n { serviceAccountJson: parsed.serviceAccountJson },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = firebaseCrashlyticsCredentials;\n\n private tokenProvider?: GcpAccessTokenProvider;\n\n private getAccessToken(signal?: AbortSignal): Promise<string> {\n this.tokenProvider ??= new GcpAccessTokenProvider({\n connectorId: this.id,\n scope: BQ_READONLY_SCOPE,\n getServiceAccountJson: () => this.creds.serviceAccountJson,\n post: (url, opts) =>\n this.post<{ access_token: string; expires_in?: number }>(url, opts),\n });\n return this.tokenProvider.getToken(signal);\n }\n\n private async fetchBigQueryPage(\n resource: ResourceName,\n request: BqPageRequest,\n signal: AbortSignal | undefined,\n ): Promise<BqQueryResponse> {\n const accessToken = await this.getAccessToken(signal);\n const headers = {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent(this.id),\n };\n if (request.method === 'POST') {\n const res = await this.post<BqQueryResponse>(request.url, {\n resource,\n headers,\n body: request.body,\n signal,\n });\n return res.body;\n }\n const res = await this.get<BqQueryResponse>(request.url, {\n resource,\n headers,\n signal,\n });\n return res.body;\n }\n\n private isResourceActive(\n resource: ResourceName,\n options: SyncOptions,\n ): boolean {\n if (!options.resources) {\n return true;\n }\n return options.resources.has(resource);\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const dataset = this.settings.bqDataset ?? DEFAULT_BQ_DATASET;\n const window = getCrashlyticsWindow(\n options,\n this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS,\n );\n const topIssuesLimit =\n this.settings.topIssuesLimit ?? DEFAULT_TOP_ISSUES_LIMIT;\n\n if (this.isResourceActive(CRASHES_METRIC_NAME, options)) {\n if (signal?.aborted) {\n return { done: false };\n }\n const sql = buildCrashesPerDaySql({\n projectId: this.settings.projectId,\n bqDataset: dataset,\n startDate: window.startDate,\n endDate: window.endDate,\n });\n const samples = await this.collectSamples(sql, signal);\n if (signal?.aborted) {\n return { done: false };\n }\n await storage.metrics(samples, { names: [CRASHES_METRIC_NAME] });\n }\n\n if (this.isResourceActive('top_issues', options)) {\n if (signal?.aborted) {\n return { done: false };\n }\n const sql = buildTopIssuesSql({\n projectId: this.settings.projectId,\n bqDataset: dataset,\n startDate: window.startDate,\n endDate: window.endDate,\n limit: topIssuesLimit,\n });\n const entities = await this.collectIssues(sql, signal);\n if (signal?.aborted) {\n return { done: false };\n }\n await storage.entities(entities, { types: [TOP_ISSUES_ENTITY_TYPE] });\n }\n\n return { done: true };\n }\n\n private jobIncompleteMessage(): string {\n return `${this.id}: BigQuery query did not complete within the synchronous timeout (jobComplete=false). Narrow the lookbackDays so the query finishes faster.`;\n }\n\n private async collectSamples(\n sql: string,\n signal?: AbortSignal,\n ): Promise<MetricSample[]> {\n const { rows } = await collectBigQueryPages<MetricSample>({\n projectId: this.settings.projectId,\n sql,\n resource: CRASHES_METRIC_NAME,\n location: this.settings.bqLocation,\n signal,\n logger: this.logger,\n mapRows: buildCrashesSamplesFromBqResponse,\n jobIncompleteMessage: this.jobIncompleteMessage(),\n fetchPage: (request, sig) =>\n this.fetchBigQueryPage(CRASHES_METRIC_NAME, request, sig),\n });\n return rows;\n }\n\n private async collectIssues(\n sql: string,\n signal?: AbortSignal,\n ): Promise<Entity[]> {\n const { rows } = await collectBigQueryPages<Entity>({\n projectId: this.settings.projectId,\n sql,\n resource: 'top_issues',\n location: this.settings.bqLocation,\n signal,\n logger: this.logger,\n mapRows: buildTopIssuesEntitiesFromBqResponse,\n jobIncompleteMessage: this.jobIncompleteMessage(),\n fetchPage: (request, sig) =>\n this.fetchBigQueryPage('top_issues', request, sig),\n });\n return rows;\n }\n}\n\ninterface CrashlyticsWindow {\n startDate: string;\n endDate: string;\n}\n\nexport function buildCrashesPerDaySql(args: {\n projectId: string;\n bqDataset: string;\n startDate: string;\n endDate: string;\n}): string {\n const table = `\\`${args.projectId}.${args.bqDataset}.*\\``;\n return [\n 'WITH events AS (',\n ' SELECT',\n ' DATE(event_timestamp) AS date,',\n ' application.bundle_id AS app_id,',\n \" LOWER(IFNULL(application.platform, 'unknown')) AS platform,\",\n ' application.app_display_version AS version,',\n ' user.id AS user_id,',\n ' is_fatal',\n ` FROM ${table}`,\n ` WHERE DATE(event_timestamp) >= DATE('${args.startDate}')`,\n ` AND DATE(event_timestamp) < DATE('${args.endDate}')`,\n ')',\n 'SELECT',\n ' date,',\n ' app_id,',\n ' platform,',\n ' version,',\n ' COUNTIF(is_fatal) AS crashes,',\n ' COUNT(DISTINCT IF(is_fatal, user_id, NULL)) AS crashing_users,',\n ' COUNT(DISTINCT user_id) AS total_users',\n 'FROM events',\n 'GROUP BY date, app_id, platform, version',\n 'ORDER BY date',\n ].join('\\n');\n}\n\nexport function buildTopIssuesSql(args: {\n projectId: string;\n bqDataset: string;\n startDate: string;\n endDate: string;\n limit: number;\n}): string {\n const table = `\\`${args.projectId}.${args.bqDataset}.*\\``;\n return [\n 'SELECT',\n ' issue_id,',\n ' ANY_VALUE(issue_title HAVING MAX event_timestamp) AS title,',\n ' ANY_VALUE(issue_subtitle HAVING MAX event_timestamp) AS subtitle,',\n ' ANY_VALUE(application.bundle_id HAVING MAX event_timestamp) AS app_id,',\n \" LOWER(IFNULL(ANY_VALUE(application.platform HAVING MAX event_timestamp), 'unknown')) AS platform,\",\n ' COUNT(*) AS event_count,',\n ' COUNT(DISTINCT user.id) AS user_count,',\n ' MAX(event_timestamp) AS last_seen',\n `FROM ${table}`,\n `WHERE DATE(event_timestamp) >= DATE('${args.startDate}')`,\n ` AND DATE(event_timestamp) < DATE('${args.endDate}')`,\n ' AND issue_id IS NOT NULL',\n 'GROUP BY issue_id',\n 'ORDER BY event_count DESC, last_seen DESC, issue_id ASC',\n `LIMIT ${args.limit}`,\n ].join('\\n');\n}\n\nexport function getCrashlyticsWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): CrashlyticsWindow {\n const endMs = startOfUtcDay(now) + MS_PER_DAY;\n let days = lookbackDays;\n if (options.mode === 'latest') {\n days = INCREMENTAL_LOOKBACK_DAYS;\n } else if (options.since !== undefined) {\n const sinceMs = parseEpoch(options.since, 'iso');\n if (sinceMs !== null) {\n const elapsed = Math.ceil((now - sinceMs) / MS_PER_DAY);\n days = Math.min(\n Math.max(elapsed + INCREMENTAL_LOOKBACK_DAYS, 1),\n lookbackDays,\n );\n }\n }\n return {\n startDate: toDateStr(endMs - days * MS_PER_DAY),\n endDate: toDateStr(endMs),\n };\n}\n\nexport function buildCrashesSamplesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n): MetricSample[] {\n const fieldIndex = indexBqFields(response);\n\n const samples: MetricSample[] = [];\n for (const row of response.rows ?? []) {\n const dateValue = readCell(row.f, fieldIndex, 'date');\n if (dateValue === null) {\n continue;\n }\n const ts = parseBqDateOrEpoch(dateValue);\n if (ts === null) {\n continue;\n }\n const crashesRaw = readCell(row.f, fieldIndex, 'crashes');\n if (crashesRaw === null) {\n continue;\n }\n const crashes = Number.parseFloat(crashesRaw);\n if (!Number.isFinite(crashes)) {\n continue;\n }\n const crashingUsersRaw = readCell(row.f, fieldIndex, 'crashing_users');\n const totalUsersRaw = readCell(row.f, fieldIndex, 'total_users');\n const crashingUsers =\n crashingUsersRaw !== null ? Number.parseFloat(crashingUsersRaw) : NaN;\n const totalUsers =\n totalUsersRaw !== null ? Number.parseFloat(totalUsersRaw) : NaN;\n\n let crashFreeRate: number | null = null;\n if (\n Number.isFinite(totalUsers) &&\n totalUsers > 0 &&\n Number.isFinite(crashingUsers)\n ) {\n const rate = 1 - crashingUsers / totalUsers;\n crashFreeRate = Math.max(0, Math.min(1, rate));\n }\n\n const attributes: Record<string, JSONValue> = {};\n const appId = readCell(row.f, fieldIndex, 'app_id');\n const platform = readCell(row.f, fieldIndex, 'platform');\n const version = readCell(row.f, fieldIndex, 'version');\n attributes['app_id'] = appId;\n attributes['platform'] = platform;\n attributes['version'] = version;\n attributes['crash_free_user_rate'] = crashFreeRate;\n attributes['crashing_users'] = Number.isFinite(crashingUsers)\n ? crashingUsers\n : null;\n\n samples.push({\n name: CRASHES_METRIC_NAME,\n ts,\n value: crashes,\n attributes,\n });\n }\n return samples;\n}\n\nexport function buildTopIssuesEntitiesFromBqResponse(\n response: z.infer<typeof bqQueryResponseSchema>,\n): Entity[] {\n const fieldIndex = indexBqFields(response);\n\n const entities: Entity[] = [];\n for (const row of response.rows ?? []) {\n const issueId = readCell(row.f, fieldIndex, 'issue_id');\n if (issueId === null || issueId.length === 0) {\n continue;\n }\n const eventCountRaw = readCell(row.f, fieldIndex, 'event_count');\n const userCountRaw = readCell(row.f, fieldIndex, 'user_count');\n const eventCount =\n eventCountRaw !== null ? Number.parseFloat(eventCountRaw) : NaN;\n const userCount =\n userCountRaw !== null ? Number.parseFloat(userCountRaw) : NaN;\n const lastSeenRaw = readCell(row.f, fieldIndex, 'last_seen');\n const lastSeenMs =\n lastSeenRaw !== null ? parseBqDateOrEpoch(lastSeenRaw) : null;\n const updatedAt = lastSeenMs ?? Date.now();\n\n const attributes: Record<string, JSONValue> = {\n issue_id: issueId,\n title: readCell(row.f, fieldIndex, 'title'),\n subtitle: readCell(row.f, fieldIndex, 'subtitle'),\n app_id: readCell(row.f, fieldIndex, 'app_id'),\n platform: readCell(row.f, fieldIndex, 'platform'),\n event_count: Number.isFinite(eventCount) ? eventCount : 0,\n user_count: Number.isFinite(userCount) ? userCount : 0,\n last_seen:\n lastSeenMs !== null ? new Date(lastSeenMs).toISOString() : null,\n };\n\n entities.push({\n type: TOP_ISSUES_ENTITY_TYPE,\n id: issueId,\n attributes,\n updated_at: updatedAt,\n });\n }\n return entities;\n}\n","import { FirebaseCrashlyticsConnector } from './firebase-crashlytics';\n\nexport {\n FirebaseCrashlyticsConnector,\n buildCrashesPerDaySql,\n buildCrashesSamplesFromBqResponse,\n buildTopIssuesEntitiesFromBqResponse,\n buildTopIssuesSql,\n configFields,\n doc,\n getCrashlyticsWindow,\n id,\n firebaseCrashlyticsResources as resources,\n} from './firebase-crashlytics';\nexport type { FirebaseCrashlyticsSettings } from './firebase-crashlytics';\nexport default FirebaseCrashlyticsConnector;\n"],"mappings":"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","names":["z","url","HTTP_CLIENT_VERSION","DEFAULT_USER_AGENT","parseEpoch","z","res","parseEpoch"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rawdash/connector-firebase-crashlytics",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
4
4
  "description": "Rawdash connector for Firebase Crashlytics - syncs daily crash counts, crash-free user rate, and top issues from the Crashlytics -> BigQuery export",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -22,23 +22,23 @@
22
22
  "import": "./dist/index.js"
23
23
  }
24
24
  },
25
- "scripts": {
26
- "build": "tsup",
27
- "typecheck": "tsc --noEmit",
28
- "lint": "eslint src",
29
- "test": "vitest run"
30
- },
31
25
  "dependencies": {
32
- "@rawdash/core": "workspace:*",
33
- "zod": "^4.4.3"
26
+ "zod": "^4.4.3",
27
+ "@rawdash/core": "0.22.0"
34
28
  },
35
29
  "devDependencies": {
36
- "@rawdash/connector-gcp-shared": "workspace:*",
37
- "@rawdash/connector-shared": "workspace:*",
38
- "@rawdash/connector-test-utils": "workspace:*",
39
30
  "fast-check": "^4.8.0",
40
31
  "tsup": "^8.0.0",
41
32
  "typescript": "^5.7.2",
42
- "vitest": "^4.1.4"
33
+ "vitest": "^4.1.4",
34
+ "@rawdash/connector-gcp-shared": "0.1.0",
35
+ "@rawdash/connector-shared": "0.3.1",
36
+ "@rawdash/connector-test-utils": "0.0.10"
37
+ },
38
+ "scripts": {
39
+ "build": "tsup",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint src",
42
+ "test": "vitest run"
43
43
  }
44
- }
44
+ }