Description
Problem Statement
Solution Brainstorm
Rough working implementation:
import {
captureException,
continueTrace,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
startSpan,
withIsolationScope,
} from 'https://deno.land/x/[email protected]/index.mjs'
import type { Integration, IntegrationFn, SpanAttributes } from 'npm:@sentry/[email protected]'
type PartialURL = {
host?: string
path?: string
protocol?: string
relative?: string
search?: string
hash?: string
}
/**
* Parses string form of URL into an object
* // borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
* // intentionally using regex and not <a/> href parsing trick because React Native and other
* // environments where DOM might not be available
* @returns parsed URL object
*/
export function parseUrl(url: string): PartialURL {
if (!url) {
return {}
}
const match = url.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/)
if (!match) {
return {}
}
// coerce to undefined values to empty string so we don't get 'undefined'
const query = match[6] || ''
const fragment = match[8] || ''
return {
host: match[4],
path: match[5],
protocol: match[2],
search: query,
hash: fragment,
relative: match[5] + query + fragment, // everything minus origin
}
}
/**
* Takes a URL object and returns a sanitized string which is safe to use as span name
* see: https://develop.sentry.dev/sdk/data-handling/#structuring-data
*/
function getSanitizedUrlString(url: PartialURL): string {
const { protocol, host, path } = url
const filteredHost = (host &&
host
// Always filter out authority
.replace(/^.*@/, '[filtered]:[filtered]@')
// Don't show standard :80 (http) and :443 (https) ports to reduce the noise
// TODO: Use new URL global if it exists
.replace(/(:80)$/, '')
.replace(/(:443)$/, '')) ||
''
return `${protocol ? `${protocol}://` : ''}${filteredHost}${path}`
}
function defineIntegration<Fn extends IntegrationFn>(fn: Fn): (...args: Parameters<Fn>) => Integration {
return fn
}
type RawHandler = (request: Request, info: Deno.ServeHandlerInfo) => Response | Promise<Response>
const INTEGRATION_NAME = 'DenoServer'
const _denoServerIntegration = (() => {
return {
name: INTEGRATION_NAME,
setupOnce() {
instrumentDenoServe()
},
}
}) satisfies IntegrationFn
/**
* Instruments `Deno.serve` to automatically create transactions and capture errors.
*
* ```js
* Sentry.init({
* integrations: [
* Sentry.denoServerIntegration(),
* ],
* })
* ```
*/
export const denoServerIntegration = defineIntegration(_denoServerIntegration)
/**
* Instruments Deno.serve by patching it's options.
*/
export function instrumentDenoServe(): void {
Deno.serve = new Proxy(Deno.serve, {
apply(serveTarget, serveThisArg, serveArgs: any) {
const [arg1, arg2] = serveArgs
let handler: RawHandler | undefined
let type = 0
if (typeof arg1 === 'function') {
handler = arg1
type = 1
} else if (typeof arg2 === 'function') {
handler = arg2
type = 2
} else if (arg1 && typeof arg1 === 'object' && 'handler' in arg1 && typeof arg1.handler === 'function') {
handler = arg1.handler
type = 3
} else if (arg2 && typeof arg2 === 'object' && 'handler' in arg2 && typeof arg2.handler === 'function') {
handler = arg2.handler
type = 4
}
if (handler) {
handler = instrumentDenoServeOptions(handler)
if (type === 1) {
serveArgs[0] = handler
} else if (type === 2) {
serveArgs[1] = handler
} else if (type === 3) {
serveArgs[0].handler = handler
} else if (type === 4) {
serveArgs[1].handler = handler
}
}
return serveTarget.apply(serveThisArg, serveArgs)
},
})
}
/**
* Instruments Deno.serve `fetch` option to automatically create spans and capture errors.
*/
function instrumentDenoServeOptions(handler: RawHandler): RawHandler {
return new Proxy(handler, {
apply(fetchTarget, fetchThisArg, fetchArgs: Parameters<typeof handler>) {
return withIsolationScope((isolationScope) => {
const request = fetchArgs[0]
const upperCaseMethod = request.method.toUpperCase()
if (upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD') {
return fetchTarget.apply(fetchThisArg, fetchArgs)
}
const parsedUrl = parseUrl(request.url)
const attributes: SpanAttributes = {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.deno.serve',
'http.request.method': request.method || 'GET',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
}
if (parsedUrl.search) {
attributes['http.query'] = parsedUrl.search
}
const url = getSanitizedUrlString(parsedUrl)
isolationScope.setSDKProcessingMetadata({
request: {
url,
method: request.method,
headers: Object.fromEntries(request.headers),
},
})
return continueTrace({
sentryTrace: request.headers.get('sentry-trace') || '',
baggage: request.headers.get('baggage'),
}, () => {
return startSpan(
{
attributes,
op: 'http.server',
name: `${request.method} ${parsedUrl.path || '/'}`,
},
async (span) => {
try {
const response = await (fetchTarget.apply(fetchThisArg, fetchArgs) as ReturnType<typeof handler>)
if (response && response.status) {
setHttpStatus(span, response.status)
isolationScope.setContext('response', {
headers: Object.fromEntries(response.headers),
status_code: response.status,
})
}
return response
} catch (e) {
captureException(e, {
mechanism: {
type: 'deno',
handled: false,
data: {
function: 'serve',
},
},
})
throw e
}
},
)
})
})
},
})
}
needs requestDataIntegration
Metadata
Metadata
Assignees
Type
Projects
Status
No status