Repository navigation
Closed
Labels
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/sentry@8.8.0/index.mjs'
import type { Integration, IntegrationFn, SpanAttributes } from 'npm:@sentry/types@8.8.0'
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
Labels
Projects
- StatusShow more project fieldsNo status
Activity
Lms24 commentedon Jun 11, 2024
Hey @brc-dd thanks for writing in! Sounds like a good idea. However, we're all booked on a lot of other tasks at the moment so I'm gonna backlog this for now. Since you already figured out most the moving parts, are you interested in contributing your instrumentation as a PR?
brc-dd commentedon Jun 11, 2024
Ok sure. I'm currently testing this out internally. I'll create a PR in 2-3 days.
I need bit of clarification though, should breadcrumbs be cleared for
isolationScopeat the starting ofwithIsolationScope? Because otherwise each message will show console logs from other requests too. Not sure though why it isn't there in bun. Something like this (https://stackoverflow.com/a/54815332/11613622):AbhiPrasad commentedon Jun 11, 2024
withIsolationScopeshould isolate the breadcrumbs to each request, you shouldn't need to clear anything.I wonder if the Deno async context is working properly 🤔
brc-dd commentedon Jun 11, 2024
Without .clear there, the breadcrumbs have older logs too:
Code:
30 remaining items
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
feat(deno): instrument Deno.serve with async context support (#19230)
github-actions commentedon Feb 24, 2026
A PR closing this issue has just been released 🚀
This issue was referenced by PR #19230, which was included in the 10.40.0 release.