newrelic
Version:
New Relic agent
574 lines (506 loc) • 18.5 kB
JavaScript
'use strict'
var util = require('util')
var properties = require('../util/properties')
var shimmer = require('../shimmer')
/**
* @namespace Library.Spec
*
* @property {string} name
* The name of this promise library.
*
* @property {?string} constructor
* Optional. The name of the property that is the Promise constructor. Default
* is to use the library itself as the Promise constructor.
*
* @property {?bool} executor
* Optional. If true, the Promise constructor itself will be wrapped for the
* executor. If false then `_proto`, `_static`, or `_library` must have an
* `executor` field whose value is the name of the executor function. Default
* is false.
*
* @property {Library.Spec.Mapping} $proto
* The mapping for Promise instance method concepts (i.e. `then`). These are
* mapped on the Promise class' prototype.
*
* @property {Library.Spec.Mapping} $static
* The mapping for Promise static method concepts (i.e. `all`, `race`). These
* are mapped on the Promise class itself.
*
* @property {?Library.Spec.Mapping} $library
* The mapping for library-level static method concepts (i.e. `fcall`, `when`).
* These are mapped on the library containing the Promise class. NOTE: in most
* promise implementations, the Promise class is itself the library thus this
* property is unnecessary.
*/
/**
* @namespace Library.Spec.Mapping
*
* @desc
* A mapping of promise concepts (i.e. `then`) to this library's implementation
* name(s) (i.e. `["then", "chain"]`). Each value can by either a single string
* or an array of strings if the concept exists under multiple keys. If any
* given concept doesn't exist in this library, it is simply skipped.
*
* @property {array} $copy
* An array of properties or methods to just directly copy without wrapping.
* This field only matters when `Library.Spec.executor` is `true`.
*
* @property {string|array} executor
*
*
* @property {string|array} then
*
*
* @property {string|array} all
*
*
* @property {string|array} race
*
*
* @property {string|array} resolve
* Indicates methods to wrap which are resolve factories. This method only
* requires wrapping if the library doesn't use an executor internally to
* implement it.
*
* @property {string|array} reject
* Indicates methods to wrap which are reject factories. Like `resolve`, this
* method only requires wrapping if the library doesn't use an executor
* internally to implement it.
*/
/**
* Instruments a promise library.
*
* @param {Agent} agent - The New Relic APM agent.
* @param {function} library - The promise library.
* @param {?Library.Spec} spec - Spec for this promise library mapping.
*/
module.exports = function initialize(agent, library, spec) {
if (spec.useFinally == null) {
spec.useFinally = true
}
// Wrap library-level methods.
wrapStaticMethods(library, spec.name, spec.$library)
// Wrap prototype methods.
var Promise = library[spec.constructor]
wrapPrototype(Promise.prototype)
wrapStaticMethods(Promise, spec.constructor, spec.$static)
// See if we are wrapping the class itself.
if (spec.executor) {
shimmer.wrapMethod(library, spec.name, spec.constructor, wrapPromise)
}
/**
* Wraps the Promise constructor as the executor.
*/
function wrapPromise() {
// Copy all unwrapped properties over.
if (spec.$static && spec.$static.$copy) {
spec.$static.$copy.forEach(function copyKeys(key) {
if (!wrappedPromise[key]) {
wrappedPromise[key] = Promise[key]
}
})
}
var passThrough = spec.$static && spec.$static.$passThrough
if (passThrough) {
passThrough.forEach(function assignProxy(proxyProp) {
if (!properties.hasOwn(wrappedPromise, proxyProp)) {
Object.defineProperty(wrappedPromise, proxyProp, {
enumerable: true,
configurable: true,
get: function getOriginal() {
return Promise[proxyProp]
},
set: function setOriginal(newValue) {
Promise[proxyProp] = newValue
}
})
}
})
}
// Inherit to pass `instanceof` checks.
util.inherits(wrappedPromise, Promise)
// Make the wrapper.
return wrappedPromise
}
function wrappedPromise(executor) {
if (!(this instanceof wrappedPromise)) {
return Promise(executor) // eslint-disable-line new-cap
}
var parent = agent.tracer.segment
var promise = null
if (
!parent ||
!parent.transaction.isActive() ||
typeof executor !== 'function' ||
arguments.length !== 1
) {
// We are expecting one function argument for executor, anything else is
// non-standard, do not attempt to wrap. Also do not attempt to wrap if we
// are not in a transaction.
var cnstrctArgs = agent.tracer.slice(arguments)
cnstrctArgs.unshift(Promise) // `unshift` === `push_front`
promise = new (Promise.bind.apply(Promise, cnstrctArgs))()
} else {
var segmentName = 'Promise ' + (executor.name || '<anonymous>')
var context = {
promise: null,
self: null,
args: null
}
promise = new Promise(wrapExecutorContext(context))
context.promise = promise
var segment = _createSegment(segmentName)
Contextualizer.link(null, promise, segment, spec.useFinally)
agent.tracer.segment = segment
segment.start()
try {
// Must run after promise is defined so that `__NR_wrapper` can be set.
executor.apply(context.self, context.args)
} catch (e) {
context.args[1](e)
} finally {
agent.tracer.segment = parent
segment.touch()
}
}
// The Promise must be created using the "real" Promise constructor (using
// normal Promise.apply(this) method does not work). But the prototype
// chain must include the wrappedPromise.prototype, V8's promise
// implementation uses promise.constructor to create new Promises for
// calls to `then`, `chain` and `catch` which allows these Promises to
// also be instrumented.
promise.__proto__ = wrappedPromise.prototype // eslint-disable-line no-proto
return promise
}
function wrapPrototype(PromiseProto, name) {
// Don't wrap the proto if there is no spec for it.
if (!spec.$proto) {
return
}
name = name || (spec.constructor + '.prototype')
// Wrap up instance methods.
_safeWrap(PromiseProto, name, spec.$proto.executor, wrapExecutorCaller)
_safeWrap(PromiseProto, name, spec.$proto.then, wrapThen)
_safeWrap(PromiseProto, name, spec.$proto.cast, wrapCast)
_safeWrap(PromiseProto, name, spec.$proto.catch, wrapCatch)
}
function wrapStaticMethods(lib, name, staticSpec) {
// Don't bother with empty specs.
if (!staticSpec) {
return
}
_safeWrap(lib, name, staticSpec.cast, wrapCast)
_safeWrap(lib, name, staticSpec.promisify, wrapPromisifiy)
}
function wrapExecutorCaller(caller) {
return function wrappedExecutorCaller(executor) {
var parent = agent.tracer.getSegment()
if (!(this instanceof Promise) || !parent || !parent.transaction.isActive()) {
return caller.apply(this, arguments)
}
var context = {
promise: this,
self: null,
args: null
}
if (!this.__NR_context) {
var segmentName = 'Promise ' + executor.name || '<anonymous>'
var segment = _createSegment(segmentName)
Contextualizer.link(null, this, segment, spec.useFinally)
}
var args = [].slice.call(arguments)
args[0] = wrapExecutorContext(context, this.__NR_context.getSegment())
var ret = caller.apply(this, args)
// Bluebird catches executor errors and auto-rejects when it catches them,
// thus we need to do so as well.
//
// When adding new libraries, make sure to check that they behave the same
// way. We may need to enhance the promise spec to handle this variance.
try {
executor.apply(context.self, context.args)
} catch (e) {
context.args[1](e)
}
return ret
}
}
/**
* Creates a function which will export the context and arguments of its
* execution.
*
* @param {object} context - The object to export the execution context with.
*
* @return {function} A function which, when executed, will add its context
* and arguments to the `context` parameter.
*/
function wrapExecutorContext(context) {
return function contextExporter(resolve, reject) {
context.self = this
context.args = [].slice.call(arguments)
context.args[0] = wrapResolver(context, resolve)
context.args[1] = wrapResolver(context, reject)
}
}
function wrapResolver(context, fn) {
return function wrappedResolveReject(val) {
var promise = context.promise
if (promise && promise.__NR_context) {
promise.__NR_context.getSegment().touch()
}
fn(val)
}
}
/**
* Creates a wrapper for `Promise#then` that extends the transaction context.
*
* @return {function} A wrapped version of `Promise#then`.
*/
function wrapThen(then, name) {
return _wrapThen(then, name, true)
}
/**
* Creates a wrapper for `Promise#catch` that extends the transaction context.
*
* @return {function} A wrapped version of `Promise#catch`.
*/
function wrapCatch(cach, name) {
return _wrapThen(cach, name, false)
}
/**
* Creates a wrapper for promise chain extending methods.
*
* @param {function} then
* The function we are to wrap as a chain extender.
*
* @param {bool} useAllParams
* When true, all parameters which are functions will be wrapped. Otherwise,
* only the last parameter will be wrapped.
*
* @return {function} A wrapped version of the function.
*/
function _wrapThen(then, name, useAllParams) {
// Don't wrap non-functions.
if (typeof then !== 'function' || then.name === '__NR_wrappedThen') {
return then
}
return function __NR_wrappedThen() {
if (!(this instanceof Promise)) {
return then.apply(this, arguments)
}
var segmentNamePrefix = 'Promise#' + name + ' '
var thenSegment = agent.tracer.getSegment()
var promise = this
// Wrap up the arguments and execute the real then.
var isWrapped = false
var args = [].map.call(arguments, wrapHandler)
var next = then.apply(this, args)
// If we got a promise (which we should have), link the parent's context.
if (!isWrapped && next instanceof Promise && next !== promise) {
Contextualizer.link(promise, next, thenSegment, spec.useFinally)
}
return next
function wrapHandler(fn, i, arr) {
if (
typeof fn !== 'function' || // Not a function
fn.name === '__NR_wrappedThenHandler' || // Already wrapped
(!useAllParams && i !== (arr.length - 1)) // Don't want all and not last
) {
isWrapped = fn && fn.name === '__NR_wrappedThenHandler'
return fn
}
return function __NR_wrappedThenHandler() {
if (!next || !next.__NR_context) {
return fn.apply(this, arguments)
}
var promSegment = next.__NR_context.getSegment()
var segmentName = segmentNamePrefix + (fn.name || '<anonymous>')
var segment = _createSegment(segmentName, promSegment)
if (segment && segment !== promSegment) {
next.__NR_context.setSegment(segment)
promSegment = segment
}
var ret = null
try {
ret = agent.tracer.bindFunction(fn, promSegment, true).apply(this, arguments)
} finally {
if (ret && typeof ret.then === 'function') {
ret = next.__NR_context.continue(ret)
}
}
return ret
}
}
}
}
/**
* Creates a wrapper around the static `Promise` factory method.
*/
function wrapCast(cast, name) {
if (typeof cast !== 'function' || cast.name === '__NR_wrappedCast') {
return cast
}
var CAST_SEGMENT_NAME = 'Promise.' + name
return function __NR_wrappedCast() {
var segment = _createSegment(CAST_SEGMENT_NAME)
var prom = cast.apply(this, arguments)
if (segment) {
Contextualizer.link(null, prom, segment, spec.useFinally)
}
return prom
}
}
function wrapPromisifiy(promisify, name) {
if (typeof promisify !== 'function' || promisify.name === '__NR_wrappedPromisify') {
return promisify
}
var WRAP_SEGMENT_NAME = 'Promise.' + name
return function __NR_wrappedPromisify() {
var promisified = promisify.apply(this, arguments)
if (typeof promisified !== 'function') {
return promisified
}
Object.keys(promisified).forEach(function forEachProperty(prop) {
__NR_wrappedPromisified[prop] = promisified[prop]
})
return __NR_wrappedPromisified
function __NR_wrappedPromisified() {
var segment = _createSegment(WRAP_SEGMENT_NAME)
var prom = agent.tracer.bindFunction(promisified, segment, true)
.apply(this, arguments)
if (segment) {
Contextualizer.link(null, prom, segment, spec.useFinally)
}
return prom
}
}
}
function _createSegment(name, parent) {
return agent.config.feature_flag.promise_segments === true
? agent.tracer.createSegment(name, null, parent)
: (parent || agent.tracer.getSegment())
}
}
/**
* Performs a `wrapMethod` if and only if `methods` is truthy and has a length
* greater than zero.
*
* @param {object} obj - The source of the methods to wrap.
* @param {string} name - The name of this source.
* @param {string|array} methods - The names of the methods to wrap.
* @param {function} wrapper - The function which wraps the methods.
*/
function _safeWrap(obj, name, methods, wrapper) {
if (methods && methods.length) {
shimmer.wrapMethod(obj, name, methods, wrapper)
}
}
function Context(segment) {
this.segments = [segment]
}
Context.prototype = Object.create(null)
Context.prototype.branch = function branch() {
return this.segments.push(null) - 1
}
function Contextualizer(idx, context, useFinally) {
this.parentIdx = -1
this.idx = idx
this.context = context
this.child = null
this.useFinally = useFinally
}
module.exports.Contextualizer = Contextualizer
Contextualizer.link = function link(prev, next, segment, useFinally) {
var ctxlzr = prev && prev.__NR_context
if (ctxlzr && !ctxlzr.isActive()) {
ctxlzr = prev.__NR_context = null
}
if (ctxlzr) {
// If prev has one child already, branch the context and update the child.
if (ctxlzr.child) {
// When the branch-point is the 2nd through nth link in the chain, it is
// necessary to track its segment separately so the branches can parent
// their segments on the branch-point.
if (ctxlzr.parentIdx !== -1) {
ctxlzr.idx = ctxlzr.context.branch()
}
// The first child needs to be updated to have its own branch as well. And
// each of that child's children must be updated with the new parent index.
// This is the only non-constant-time action for linking, but it only
// happens with branching promise chains specifically when the 2nd branch
// is added.
//
// Note: This does not account for branches of branches. That may result
// in improperly parented segments.
var parent = ctxlzr
var child = ctxlzr.child
var branchIdx = ctxlzr.context.branch()
do {
child.parentIdx = parent.idx
child.idx = branchIdx
parent = child
child = child.child
} while (child)
// We set the child to something falsey that isn't `null` so we can
// distinguish between having no child, having one child, and having
// multiple children.
ctxlzr.child = false
}
// If this is a branching link then create a new branch for the next promise.
// Otherwise, we can just piggy-back on the previous link's spot.
var idx = ctxlzr.child === false ? ctxlzr.context.branch() : ctxlzr.idx
// Create a new context for this next promise.
next.__NR_context = new Contextualizer(idx, ctxlzr.context, ctxlzr.useFinally)
next.__NR_context.parentIdx = ctxlzr.idx
// If this was our first child, remember it in case we have a 2nd.
if (ctxlzr.child === null) {
ctxlzr.child = next.__NR_context
}
} else if (segment) {
// This next promise is the root of a chain. Either there was no previous
// promise or the promise was created out of context.
next.__NR_context = new Contextualizer(0, new Context(segment), useFinally)
}
}
Contextualizer.prototype = Object.create(null)
Contextualizer.prototype.isActive = function isActive() {
var segments = this.context.segments
var segment = segments[this.idx] || segments[this.parentIdx] || segments[0]
return segment && segment.transaction.isActive()
}
Contextualizer.prototype.getSegment = function getSegment() {
var segments = this.context.segments
var segment = segments[this.idx]
if (segment == null) {
segment = segments[this.idx] = segments[this.parentIdx] || segments[0]
}
return segment
}
Contextualizer.prototype.setSegment = function setSegment(segment) {
return this.context.segments[this.idx] = segment
}
Contextualizer.prototype.toJSON = function toJSON() {
// No-op.
}
Contextualizer.prototype.continue = function continueContext(prom) {
var self = this
var nextContext = prom.__NR_context
if (!nextContext) {
return prom
}
// If we have `finally`, use that to sneak our context update.
if (typeof prom.finally === 'function' && nextContext.useFinally) {
return prom.finally(__NR_continueContext)
}
// No `finally` means we need to hook into resolve and reject individually and
// pass through whatever happened.
return prom.then(function __NR_thenContext(val) {
__NR_continueContext()
return val
}, function __NR_catchContext(err) {
__NR_continueContext()
throw err // Re-throwing promise rejection, this is not New Relic's error.
})
function __NR_continueContext() {
self.setSegment(nextContext.getSegment())
}
}