newrelic
Version:
New Relic agent
178 lines (156 loc) • 5.75 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
const symbols = require('../../symbols')
const { MiddlewareSpec, MiddlewareMounterSpec } = require('../../shim/specs')
module.exports = function initialize(shim, Koa) {
// Koa's exports are different depending on using CJS or MJS - https://github.com/koajs/koa/issues/1513
const proto = Koa.prototype || Koa.default?.prototype
if (!shim || !Koa || !proto || Object.keys(proto).length > 1) {
shim.logger.debug(
'Koa instrumentation function called with incorrect arguments, not instrumenting.'
)
return
}
shim.setFramework(shim.KOA)
shim.wrapMiddlewareMounter(
proto,
'use',
new MiddlewareMounterSpec({
wrapper: wrapMiddleware
})
)
shim.wrapReturn(proto, 'createContext', wrapCreateContext)
// The application is used to handle unhandled errors in the application. We
// want to notice those.
shim.wrap(proto, 'emit', function wrapper(shim, original) {
return function wrappedEmit(evt, err, ctx) {
if (evt === 'error' && ctx) {
shim.noticeError(ctx.req, err)
}
return original.apply(this, arguments)
}
})
}
function wrapMiddleware(shim, middleware) {
// Skip middleware that are already wrapped.
if (shim.isWrapped(middleware)) {
return middleware
}
if (middleware.router) {
shim.logger.info(
[
'Found uninstrumented router property on Koa middleware.',
'This may indicate either an unsupported routing library is being used,',
'or a particular version of a supported library is not fully instrumented.'
].join(' ')
)
}
return shim.recordMiddleware(
middleware,
new MiddlewareSpec({
type: shim.MIDDLEWARE,
promise: true,
appendPath: true,
next: shim.LAST,
req: function getReq(shim, fn, fnName, args) {
return args[0] && args[0].req
}
})
)
}
/**
* Many of the properties on the `context` object are just aliases for the same
* property on the `request` or `response` objects. We take advantage of this
* by just intercepting the `request` or `response` property and don't touch
* the `context` property.
* See: https://github.com/koajs/koa/blob/master/lib/context.js#L186-L241
*
* @param {Shim} shim instance of shim
* @param {function} _fn createContext function
* @param {string} _fnName name of function
* @param {object} context koa ctx object
*/
function wrapCreateContext(shim, _fn, _fnName, context) {
wrapResponseBody(shim, context)
wrapMatchedRoute(shim, context)
wrapResponseStatus(shim, context)
}
function wrapResponseBody(shim, context) {
// The `context.body` and `context.response.body` properties are how users set
// the response contents. It is roughly equivalent to `res.send()` in Express.
// Under the hood, these set the `_body` property on the `context.response`.
context[symbols.koaBody] = context.response.body
context[symbols.koaBodySet] = false
Object.defineProperty(context.response, '_body', {
get: () => context[symbols.koaBody],
set: function setBody(val) {
if (!context[symbols.koaRouter]) {
shim.savePossibleTransactionName(context.req)
}
context[symbols.koaBody] = val
context[symbols.koaBodySet] = true
}
})
}
function wrapMatchedRoute(shim, context) {
context[symbols.koaMatchedRoute] = null
context[symbols.koaRouter] = false
Object.defineProperty(context, '_matchedRoute', {
get: () => context[symbols.koaMatchedRoute],
set: (val) => {
// match should never be undefined given _matchedRoute was set
if (val) {
const transaction = shim.tracer.getTransaction()
// Segment/Transaction may be null, see:
// - https://github.com/newrelic/node-newrelic-koa/issues/32
// - https://github.com/newrelic/node-newrelic-koa/issues/33
if (transaction) {
if (context[symbols.koaMatchedRoute]) {
transaction.nameState.popPath()
}
transaction.nameState.appendPath(val)
transaction.nameState.markPath()
}
}
context[symbols.koaMatchedRoute] = val
// still true if somehow match is undefined because we are
// using koa-router naming and don't want to allow default naming
context[symbols.koaRouter] = true
}
})
}
function wrapResponseStatus(shim, context) {
// Sometimes people just set `context.status` or `context.response.status`
// without setting a body. When this happens we'll want to use that as the
// response point to name the transaction. `context.status` is just an alias
// for `context.response.status` so we only wrap the latter.
const statusDescriptor = getInheritedPropertyDescriptor(context.response, 'status')
if (!statusDescriptor) {
shim.logger.debug('Failed to find status descriptor on context.response')
return
} else if (!statusDescriptor.get || !statusDescriptor.set) {
shim.logger.debug(statusDescriptor, 'Status descriptor missing getter/setter pair')
return
}
Object.defineProperty(context.response, 'status', {
get: () => statusDescriptor.get.call(context.response),
set: function setStatus(val) {
if (!context[symbols.koaBodySet] && !context[symbols.koaRouter]) {
shim.savePossibleTransactionName(context.req)
}
return statusDescriptor.set.call(this, val)
}
})
}
function getInheritedPropertyDescriptor(obj, property) {
let proto = obj
let descriptor = null
do {
descriptor = Object.getOwnPropertyDescriptor(proto, property)
proto = Object.getPrototypeOf(proto)
} while (!descriptor && proto)
return descriptor
}