UNPKG

edgerender

Version:
155 lines (141 loc) 4.28 kB
export function setupSentry(dsn: string, environment?: string, release?: string): Sentry | undefined { const m = dsn.match(/^https:\/\/(.+?)@(.+?)\.ingest\.sentry\.io\/(.+)/) if (m) { const [, key, server, app] = m return new Sentry(key, server, app, environment, release) } else { console.warn('invalid sentry DSN', dsn) } } export class Sentry { readonly key: string readonly server: string readonly app: string readonly release?: string readonly environment: string constructor(key: string, server: string, app: string, environment?: string, release?: string) { this.key = key this.server = server this.app = app this.environment = environment || 'production' this.release = release } captureMessage(event: FetchEvent, message: string, {level = 'info', extra = {}} = {}): void { event.waitUntil(this.capture(event.request, {message, level, extra})) } captureException(event: FetchEvent, exc: Error, {level = 'error', extra = {}} = {}): void { const message = exc.toString() || exc.message || 'Unknown error' event.waitUntil( this.capture(event.request, { exception: { mechanism: {handled: true, type: 'generic'}, values: [ { type: 'Error', value: message, stacktrace: {frames: get_frames(exc.stack)}, }, ], }, message, level, extra, }), ) } private async capture(request: Request, data: Partial<SentryData>): Promise<Response> { const sentry_data: SentryData = Object.assign( { platform: 'javascript', logger: 'cloudflare', environment: this.environment, fingerprint: [`${request.method}-${request.url}-${data.message || 'null'}`], user: { ip_address: request.headers.get('CF-Connecting-IP'), }, request: { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), }, extra: {}, }, data, ) sentry_data.extra.cloudflare = request.cf // console.log('sentry data:', sentry_data) // if (this.release) { // defaults.release = this.release // } const params = { sentry_key: this.key, sentry_version: 7, sentry_client: 'edgerender; https://github.com/samuelcolvin/edgerender', } const args = Object.entries(params) .map(([k, v]) => `${k}=${encodeURIComponent(v)}`) .join('&') const sentry_url = `https://sentry.io/api/${this.app}/store/?${args}` return await fetch(sentry_url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(sentry_data), }) } } interface Frame { in_app: true filename: string function: string lineno?: number colno?: number } interface SentryData { platform: string logger: string environment: string fingerprint: string[] user: {ip_address: string} request: {url: string; method: string; headers: Record<string, string>} exception?: { mechanism: {handled: boolean; type: string} values: {type: string; value: string; stacktrace: {frames: Frame[]}}[] } message?: string level?: string extra: Record<string, any> } function get_frames(stack: string | undefined): Frame[] { if (!stack) { return [] } const lines = stack.split('\n') lines.splice(0, 1) return lines .reverse() .filter(line => line.match(/^ {4}at /)) .map(extract_frame) } const frame_regexes: RegExp[] = [ /^ +at (?<func>.+?) \((?<filename>.+?):(?<lineno>\d+):(?<colno>\d+)\)/, /^ +at (?<func>.+?):(?<lineno>\d+):(?<colno>\d+)/, /^ +at (?<func>.+?) \(/, ] function extract_frame(line: string): Frame { for (const regex of frame_regexes) { const m = line.match(regex) if (m) { const {func, filename, lineno, colno} = m.groups as Record<string, string> const f = { in_app: true, filename: `~/${filename || '__unknown__.js'}`, function: func, } if (lineno) { Object.assign(f, {lineno: parseInt(lineno), colno: parseInt(colno)}) } return f as Frame } } throw Error(`no frame found in stack line: "${line}"`) }