import type { IssueTriage, JsonRecord, SentryBreadcrumbSummary, SentryEvent, SentryEventSummary, SentryExceptionSummary, SentryIssue, SummarizeIssueEventOptions, } from './types.ts' export function summarizeIssueEvent( event: SentryEvent, options: SummarizeIssueEventOptions = {}, ): SentryEventSummary { const eventRecord = asRecord(event) ?? {} const metadata = asRecord(eventRecord.metadata) ?? {} const contexts = asRecord(eventRecord.contexts) ?? {} const entries = asArray(eventRecord.entries).map((entry) => asRecord(entry)).filter(Boolean) as JsonRecord[] const exceptionChain = extractExceptionChain(entries, options.maxFramesPerException ?? 8) const breadcrumbs = extractBreadcrumbs(entries, options.maxBreadcrumbs ?? 20) const tags = normalizeTags(eventRecord.tags) return { id: stringValue(eventRecord.id), eventId: stringValue(eventRecord.eventID) ?? stringValue(eventRecord.eventId), title: stringValue(eventRecord.title) ?? stringValue(metadata.title), message: stringValue(eventRecord.message) ?? stringValue(metadata.value), culprit: stringValue(eventRecord.culprit), platform: stringValue(eventRecord.platform), projectId: stringValue(eventRecord.projectID) ?? stringValue(eventRecord.projectId), groupId: stringValue(eventRecord.groupID) ?? stringValue(eventRecord.groupId), release: stringValue(eventRecord.release) ?? stringValue(tags.release), environment: stringValue(eventRecord.environment) ?? stringValue(tags.environment), timestamp: stringValue(eventRecord.dateCreated) ?? stringValue(eventRecord.dateReceived), exceptionChain, tags, breadcrumbs, contexts, contextKeys: Object.keys(contexts), user: eventRecord.user, request: eventRecord.request, } } export function summarizeIssueMetadata(issue: SentryIssue): IssueTriage['issue'] { return { id: issue.id, shortId: issue.shortId, title: issue.title, status: issue.status, level: stringValue(issue.level), permalink: issue.permalink, firstSeen: stringValue(issue.firstSeen), lastSeen: stringValue(issue.lastSeen), count: issue.count as string | number | undefined, userCount: issue.userCount as string | number | undefined, project: issue.project, assignedTo: issue.assignedTo, } } export function uniqueStrings(values: Array): string[] { return [...new Set(values.filter(Boolean) as string[])] } function extractExceptionChain(entries: JsonRecord[], maxFramesPerException: number): SentryExceptionSummary[] { return entries .filter((entry) => entry.type === 'exception') .flatMap((entry) => { const data = asRecord(entry.data) ?? {} return asArray(data.values) }) .map((exception) => { const exceptionRecord = asRecord(exception) ?? {} const stacktrace = asRecord(exceptionRecord.stacktrace) ?? {} const frames = asArray(stacktrace.frames).map((frame) => asRecord(frame)).filter(Boolean) as JsonRecord[] const selectedFrames = frames.slice(-maxFramesPerException).map(summarizeFrame) return { type: stringValue(exceptionRecord.type), value: stringValue(exceptionRecord.value), module: stringValue(exceptionRecord.module), mechanism: exceptionRecord.mechanism, frameCount: frames.length, frames: selectedFrames, } }) } function summarizeFrame(frame: JsonRecord): SentryExceptionSummary['frames'][number] { return { function: stringValue(frame.function), module: stringValue(frame.module), filename: stringValue(frame.filename), absPath: stringValue(frame.absPath) ?? stringValue(frame.abs_path), lineNo: numberValue(frame.lineNo) ?? numberValue(frame.lineno), colNo: numberValue(frame.colNo) ?? numberValue(frame.colno), inApp: booleanValue(frame.inApp) ?? booleanValue(frame.in_app), contextLine: stringValue(frame.contextLine) ?? stringValue(frame.context_line), } } function extractBreadcrumbs(entries: JsonRecord[], maxBreadcrumbs: number): SentryBreadcrumbSummary[] { const breadcrumbEntry = entries.find((entry) => entry.type === 'breadcrumbs') const data = asRecord(breadcrumbEntry?.data) ?? {} return asArray(data.values) .slice(-maxBreadcrumbs) .map((breadcrumb) => { const record = asRecord(breadcrumb) ?? {} return { timestamp: stringValue(record.timestamp), type: stringValue(record.type), category: stringValue(record.category), level: stringValue(record.level), message: stringValue(record.message), data: record.data, } }) } function normalizeTags(tags: unknown): Record { if (Array.isArray(tags)) { return Object.fromEntries( tags .map((tag) => { const record = asRecord(tag) if (!record) return null const key = stringValue(record.key) ?? stringValue(record.name) if (!key) return null return [key, record.value] }) .filter(Boolean) as Array<[string, unknown]>, ) } return asRecord(tags) ?? {} } function asArray(value: unknown): unknown[] { return Array.isArray(value) ? value : [] } function asRecord(value: unknown): JsonRecord | null { if (value && typeof value === 'object' && !Array.isArray(value)) { return value as JsonRecord } return null } function stringValue(value: unknown): string | undefined { return typeof value === 'string' && value.length > 0 ? value : undefined } function numberValue(value: unknown): number | undefined { return typeof value === 'number' && Number.isFinite(value) ? value : undefined } function booleanValue(value: unknown): boolean | undefined { return typeof value === 'boolean' ? value : undefined }