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 | undefined>): 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<string, unknown> {
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
}