diff --git a/.eslintrc.js b/.eslintrc.js index 56e61af489..fb85a9b286 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,6 +11,8 @@ module.exports = { rules: { 'consistent-return': 'off', 'jsdoc/require-jsdoc': 'off', + 'jsdoc/tag-lines': 'off', + 'jsdoc/check-types': 'off', 'jsdoc/no-undefined-types': [ 'warn', { diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 76c854c186..c66c6bb625 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -2426,7 +2426,7 @@ THE SOFTWARE. ### eslint-plugin-jsdoc -This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v39.9.1](https://github.com/gajus/eslint-plugin-jsdoc/tree/v39.9.1)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v39.9.1/LICENSE): +This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v48.0.5](https://github.com/gajus/eslint-plugin-jsdoc/tree/v48.0.5)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v48.0.5/LICENSE): ``` Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/) diff --git a/lib/instrumentation/express.js b/lib/instrumentation/express.js index 8893153961..f9dfeada31 100644 --- a/lib/instrumentation/express.js +++ b/lib/instrumentation/express.js @@ -5,9 +5,12 @@ 'use strict' +const { MiddlewareSpec, MiddlewareMounterSpec } = require('../../lib/shim/specs') +const { MIDDLEWARE_TYPE_NAMES } = require('../../lib/shim/webframework-shim/common') + /** * Express middleware generates traces where middleware are considered siblings - * (ended on 'next' invocation) and not nested. Middlware are nested below the + * (ended on 'next' invocation) and not nested. Middleware are nested below the * routers they are mounted to. */ @@ -32,15 +35,23 @@ module.exports = function initialize(agent, express, moduleName, shim) { function wrapExpress4(shim, express) { // Wrap `use` and `route` which are hung off `Router` directly, not on a // prototype. - shim.wrapMiddlewareMounter(express.Router, 'use', { - route: shim.FIRST, - wrapper: wrapMiddleware - }) - - shim.wrapMiddlewareMounter(express.application, 'use', { - route: shim.FIRST, - wrapper: wrapMiddleware - }) + shim.wrapMiddlewareMounter( + express.Router, + 'use', + new MiddlewareMounterSpec({ + route: shim.FIRST, + wrapper: wrapMiddleware + }) + ) + + shim.wrapMiddlewareMounter( + express.application, + 'use', + new MiddlewareMounterSpec({ + route: shim.FIRST, + wrapper: wrapMiddleware + }) + ) shim.wrap(express.Router, 'route', function wrapRoute(shim, fn) { if (!shim.isFunction(fn)) { @@ -61,13 +72,17 @@ function wrapExpress4(shim, express) { // This wraps a 'done' function but not a traditional 'next' function. This allows // the route to stay on the stack for middleware nesting after the router. // The segment will be automatically ended by the http/https instrumentation. - shim.recordMiddleware(layer, 'handle', { - type: shim.ROUTE, - req: shim.FIRST, - next: shim.LAST, - matchArity: true, - route: route.path - }) + shim.recordMiddleware( + layer, + 'handle', + new MiddlewareSpec({ + type: shim.ROUTE, + req: shim.FIRST, + next: shim.LAST, + matchArity: true, + route: route.path + }) + ) } return route } @@ -76,11 +91,14 @@ function wrapExpress4(shim, express) { shim.wrapMiddlewareMounter(express.Router, 'param', { route: shim.FIRST, wrapper: function wrapParamware(shim, middleware, fnName, route) { - return shim.recordParamware(middleware, { - name: route, - req: shim.FIRST, - next: shim.THIRD - }) + return shim.recordParamware( + middleware, + new MiddlewareSpec({ + name: route, + req: shim.FIRST, + next: shim.THIRD + }) + ) } }) @@ -97,21 +115,33 @@ function wrapExpress3(shim, express) { shim.wrapMiddlewareMounter(express.Router.prototype, 'param', { route: shim.FIRST, wrapper: function wrapParamware(shim, middleware, fnName, route) { - return shim.recordParamware(middleware, { - name: route, - req: shim.FIRST, - next: shim.THIRD - }) + return shim.recordParamware( + middleware, + new MiddlewareSpec({ + name: route, + req: shim.FIRST, + next: shim.THIRD, + type: MIDDLEWARE_TYPE_NAMES.PARAMWARE + }) + ) } }) - shim.wrapMiddlewareMounter(express.Router.prototype, 'use', { - route: shim.FIRST, - wrapper: wrapMiddleware - }) - shim.wrapMiddlewareMounter(express.application, 'use', { - route: shim.FIRST, - wrapper: wrapMiddleware - }) + shim.wrapMiddlewareMounter( + express.Router.prototype, + 'use', + new MiddlewareMounterSpec({ + route: shim.FIRST, + wrapper: wrapMiddleware + }) + ) + shim.wrapMiddlewareMounter( + express.application, + 'use', + new MiddlewareMounterSpec({ + route: shim.FIRST, + wrapper: wrapMiddleware + }) + ) // NOTE: Do not wrap application route methods in Express 3, they all just // forward their arguments to the router. @@ -153,12 +183,12 @@ function wrapResponse(shim, response) { function wrapMiddleware(shim, middleware, name, route) { let method = null - const spec = { + const spec = new MiddlewareSpec({ route: route, type: shim.MIDDLEWARE, matchArity: true, req: shim.FIRST - } + }) if (middleware.lazyrouter) { method = 'handle' diff --git a/lib/instrumentation/fastify/spec-builders.js b/lib/instrumentation/fastify/spec-builders.js index 7c729326b9..6e66e3b6ed 100644 --- a/lib/instrumentation/fastify/spec-builders.js +++ b/lib/instrumentation/fastify/spec-builders.js @@ -5,6 +5,8 @@ 'use strict' +const { MiddlewareSpec } = require('../../shim/specs') + /** * Retrieves the IncomingMessage from a Fastify request. Depending on the * context of this function it either exists on `request.raw` or just `request` @@ -55,7 +57,7 @@ const getParamsFromFastifyRequest = (shim, fn, fnName, args) => { * @returns {object} spec for Fastify route handler */ function buildMiddlewareSpecForRouteHandler(shim, path) { - return { + return new MiddlewareSpec({ /** * A function where we can wrap next, reply send, etc. methods * @@ -93,7 +95,7 @@ function buildMiddlewareSpecForRouteHandler(shim, path) { params: getParamsFromFastifyRequest, req: getRequestFromFastify, route: path - } + }) } /** @@ -105,14 +107,14 @@ function buildMiddlewareSpecForRouteHandler(shim, path) { * @returns {object} spec for Fastify middleware */ function buildMiddlewareSpecForMiddlewareFunction(shim, name, route) { - return { + return new MiddlewareSpec({ name, route, next: shim.LAST, params: getParamsFromFastifyRequest, req: getRequestFromFastify, type: shim.MIDDLEWARE - } + }) } module.exports = { diff --git a/lib/instrumentation/grpc-js/grpc.js b/lib/instrumentation/grpc-js/grpc.js index a50eeba1e0..e57879a4e2 100644 --- a/lib/instrumentation/grpc-js/grpc.js +++ b/lib/instrumentation/grpc-js/grpc.js @@ -7,6 +7,7 @@ const recordExternal = require('../../metrics/recorders/http_external') const recordHttp = require('../../metrics/recorders/http') +const specs = require('../../shim/specs') const { DESTINATIONS } = require('../../config/attribute-filter') const DESTINATION = DESTINATIONS.TRANS_EVENT | DESTINATIONS.ERROR_EVENT const semver = require('semver') @@ -146,7 +147,10 @@ function wrapRegister(shim, original) { return original.apply(this, arguments) } - args[1] = shim.bindCreateTransaction(instrumentedHandler, { type: shim.WEB }) + args[1] = shim.bindCreateTransaction( + instrumentedHandler, + new specs.TransactionSpec({ type: shim.WEB }) + ) return original.apply(this, args) diff --git a/lib/instrumentation/openai.js b/lib/instrumentation/openai.js index 33a082d797..20adae27a4 100644 --- a/lib/instrumentation/openai.js +++ b/lib/instrumentation/openai.js @@ -12,6 +12,7 @@ const { LlmErrorMessage } = require('../../lib/llm-events/openai') const LlmTrackedIds = require('../../lib/llm-events/tracked-ids') +const { RecorderSpec } = require('../../lib/shim/specs') const MIN_VERSION = '4.0.0' const MIN_STREAM_VERSION = '4.12.2' @@ -267,7 +268,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { return } - return { + return new RecorderSpec({ name: OPENAI.COMPLETION, promise: true, // eslint-disable-next-line max-params @@ -286,7 +287,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true) } - } + }) } ) @@ -299,7 +300,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { 'create', function wrapEmbeddingCreate(shim, embeddingCreate, name, args) { const [request] = args - return { + return new RecorderSpec({ name: OPENAI.EMBEDDING, promise: true, // eslint-disable-next-line max-params @@ -335,7 +336,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) { delete response.api_key delete response.headers } - } + }) } ) } diff --git a/lib/instrumentation/pg.js b/lib/instrumentation/pg.js index 4484064725..6431e72774 100644 --- a/lib/instrumentation/pg.js +++ b/lib/instrumentation/pg.js @@ -6,6 +6,7 @@ 'use strict' const { nrEsmProxy } = require('../symbols') +const specs = require('../shim/specs') function getQuery(shim, original, name, args) { const config = args[0] @@ -35,22 +36,22 @@ module.exports = function initialize(agent, pgsql, moduleName, shim) { // pg supports event based Client.query when a Query object is passed in, // and works similarly in pg version <7.0.0 if (typeof queryArgs[0] === 'string') { - return { + return new specs.QuerySpec({ callback: shim.LAST, query: getQuery, promise: true, parameters: getInstanceParameters(shim, this), internal: false - } + }) } - return { + return new specs.QuerySpec({ callback: shim.LAST, query: getQuery, stream: 'row', parameters: getInstanceParameters(shim, this), internal: false - } + }) } // wrapping for native @@ -73,10 +74,10 @@ module.exports = function initialize(agent, pgsql, moduleName, shim) { shim.recordQuery(this, 'query', wrapJSClientQuery) shim.record(this, 'connect', function pgConnectNamer() { - return { + return new specs.QuerySpec({ name: 'connect', callback: shim.LAST - } + }) }) } diff --git a/lib/serverless/aws-lambda.js b/lib/serverless/aws-lambda.js index f5b6739eba..3d3d2843e1 100644 --- a/lib/serverless/aws-lambda.js +++ b/lib/serverless/aws-lambda.js @@ -13,6 +13,7 @@ const recordBackground = require('../metrics/recorders/other') const recordWeb = require('../metrics/recorders/http') const TransactionShim = require('../shim/transaction-shim') const urltils = require('../util/urltils') +const specs = require('../shim/specs') // CONSTANTS const ATTR_DEST = require('../config/attribute-filter').DESTINATIONS @@ -121,7 +122,7 @@ class AwsLambda { this.wrapFatal() } - return shim.bindCreateTransaction(wrappedHandler, { type: shim.BG }) + return shim.bindCreateTransaction(wrappedHandler, new specs.TransactionSpec({ type: shim.BG })) function wrappedHandler() { const args = shim.argsToArray.apply(shim, arguments) diff --git a/lib/shim/datastore-shim.js b/lib/shim/datastore-shim.js index e127ca3a23..2b6e9025c3 100644 --- a/lib/shim/datastore-shim.js +++ b/lib/shim/datastore-shim.js @@ -134,23 +134,6 @@ DatastoreShim.prototype.getDatabaseNameFromUseQuery = getDatabaseNameFromUseQuer * @see QuerySpec */ -/** - * @callback QueryFunction - * @summary - * Pulls the query argument out from an array of arguments. - * @param {Shim} shim - * The shim this function was passed to. - * @param {Function} func - * The function being recorded. - * @param {string} name - * The name of the function. - * @param {Array.<*>} args - * The arguments being passed into the function. - * @returns {string} The query string from the arguments list. - * @see QuerySpec - * @see QuerySpecFunction - */ - /** * @callback QueryParserFunction * @summary @@ -162,67 +145,6 @@ DatastoreShim.prototype.getDatabaseNameFromUseQuery = getDatabaseNameFromUseQuer * @see ParsedQueryData */ -/** - * @interface OperationSpec - * @description - * Describes the interface for an operation function. - * @property {string} [name] - * The name for this operation. If omitted, the operation function's name will - * used instead. - * @property {DatastoreParameters} [parameters] - * Extra parameters to be set on the metric for the operation. - * @property {boolean} [record=true] - * Indicates if the operation should be recorded as a metric. A segment will be - * created even if this is `false`. - * @property {number|CallbackBindFunction} [callback] - * If a number, it is the offset in the arguments array for the operation's - * callback argument. If it is a function, it should perform the segment - * binding to the callback. - * @property {boolean} [promise=false] - * If `true`, the return value will be wrapped as a Promise. - * @see DatastoreShim#recordOperation - * @see QuerySpec - * @see DatastoreParameters - */ - -/** - * @interface QuerySpec - * @augments OperationSpec - * @description - * Describes the interface for a query function. Extends {@link OperationSpec} - * with query-specific parameters. - * @property {boolean} [stream=false] - * If `true`, the return value will be wrapped as a stream. - * @property {number|string|QueryFunction} query - * If a number, it is the offset in the arguments array for the query string - * argument. If a string, it is the query being executed. If a function, it - * will be passed the arguments and must return the query string. - * @see DatastoreShim#recordQuery - * @see DatastoreShim#recordBatchQuery - * @see QuerySpecFunction - * @see QueryFunction - * @see OperationSpec - * @see DatastoreParameters - */ - -/** - * @interface DatastoreParameters - * @description - * Extra parameters which may be added to an operation or query segment. All of - * these properties are optional. - * @property {string} host - * The host of the database server being interacted with. If provided, along - * with `port_path_or_id`, then an instance metric will also be generated for - * this database. - * @property {number|string} port_path_or_id - * The port number or path to domain socket used to connect to the database - * server. - * @property {string} database_name - * The name of the database being queried or operated on. - * @see OperationSpec - * @see QuerySpec - */ - /** * @interface ParsedQueryData * @description @@ -782,7 +704,7 @@ function _extractQueryStr(fn, fnName, spec, ctx, args) { * * @private * @this DatastoreShim - * @param {object} [parameters={}] - The segment parameters to clean up. + * @param {object} [parameters] - The segment parameters to clean up. * @returns {object} - The normalized segment parameters. */ function _normalizeParameters(parameters) { diff --git a/lib/shim/message-shim/index.js b/lib/shim/message-shim/index.js index dbb7eece8a..ac17b11ecb 100644 --- a/lib/shim/message-shim/index.js +++ b/lib/shim/message-shim/index.js @@ -49,6 +49,7 @@ const LIBRARY_TRANSPORT_TYPES = { * Enumeration of possible message broker destination types. * * @readonly + * @typedef {Object} MessageShimTypes * @memberof MessageShim * @enum {string} */ @@ -147,7 +148,7 @@ MessageShim.prototype.recordSubscribedConsume = recordSubscribedConsume /** * @callback MessageConsumerWrapperFunction * @summary - * Function that is used to wrap message consumer functions. Used along side + * Function that is used to wrap message consumer functions. Used alongside * the MessageShim#recordSubscribedConsume API method. * @param {MessageShim} shim * The shim this function was handed to. diff --git a/lib/shim/message-shim/subscribe-consume.js b/lib/shim/message-shim/subscribe-consume.js index fdec2220cb..0f1d18fd62 100644 --- a/lib/shim/message-shim/subscribe-consume.js +++ b/lib/shim/message-shim/subscribe-consume.js @@ -91,10 +91,13 @@ function makeWrapConsumer({ spec, queue, destinationName, destNameIsArg }) { } const consumerWrapper = createConsumerWrapper({ shim, consumer, cName, spec: msgDescDefaults }) - return shim.bindCreateTransaction(consumerWrapper, { - type: shim.MESSAGE, - nest: true - }) + return shim.bindCreateTransaction( + consumerWrapper, + new specs.TransactionSpec({ + type: shim.MESSAGE, + nest: true + }) + ) } } diff --git a/lib/shim/shim.js b/lib/shim/shim.js index 9e33775c15..07f2e13dfe 100644 --- a/lib/shim/shim.js +++ b/lib/shim/shim.js @@ -252,216 +252,6 @@ Shim.prototype[symbols.unwrap] = unwrapAll * @param {string} [scope] - The scope of the recording. */ -/** - * @callback ConstructorHookFunction - * @summary - * Pre/post constructor execution hook for wrapping classes. Used by - * {@link ClassWrapSpec}. - * @param {Shim} shim - * The shim performing the wrapping/binding. - * @param {Function} Base - * The class that was wrapped. - * @param {string} name - * The name of the `Base` class. - * @param {Array.<*>} args - * The arguments to the class constructor. - * @see ClassWrapSpec - */ - -/** - * @private - * @interface Spec - * @description - * The syntax for declarative instrumentation. It can be used interlaced with - * custom, hand-written instrumentation for one-off or hard to simplify - * instrumentation logic. - * @property {Spec|WrapFunction} $return - * Changes the context to the return value of the current context. This means - * the sub spec will not be executed up front, but instead upon every execution - * of the current context. - * - * ```js - * var ret = func.apply(this, args); - * return shim.wrap(ret, spec.$return) - * ``` - * @property {Spec|WrapFunction} $proto - * Changes the context to the prototype of the current context. The prototype - * is found using `Object.getPrototypeOf`. - * - * ```js - * shim.wrap(Object.getPrototypeOf(context), spec.$proto) - * ``` - * @property {boolean} $once - * Ensures that the parent spec will only be executed one time if the value is - * `true`. Good for preventing double wrapping of prototype methods. - * - * ```js - * if (spec.$once && spec[symbols.onceExecuted]) { - * return context - * } - * spec[symbols.onceExecuted] = true - * ``` - * @property {ArgumentsFunction} $arguments - * Executes the function with all of the arguments passed in. The arguments can - * be modified in place. This will execute before `$eachArgument`. - * - * ```js - * spec.$arguments(args) - * ``` - * @property {Spec|ArrayWrapFunction} $eachArgument - * Executes `shim.wrap` on each argument passed to the current context. The - * returned arguments will then be used to actually execute the function. - * - * ```js - * var argLength = arguments.length - * var extraArgs = extras.concat([0, argLength]) - * var iIdx = extraArgs.length - 2 - * var args = new Array(argLength) - * for (var i = 0; i < argLength; ++i) { - * extraArgs[iIdx] = i - * args[i] = shim.wrap(arguments[i], spec.$eachArgument, extraArgs) - * } - * func.apply(this, args) - * ``` - * @property {Array.<{$properties: Array., $spec: Spec}>} $wrappings - * Executes `shim.wrap` with the current context as the `nodule` for each - * element in the array. The `$properties` sub-key must list one or more - * properties to be wrapped. The `$spec` sub-key must be a {@link Spec} or - * {@link WrapFunction} for wrapping the properties. - * - * ```js - * spec.$wrappings.forEach(function($wrap) { - * shim.wrap(context, $wrap.$properties, $wrap.$spec) - * }) - * ``` - * @property {boolean|string|SegmentFunction} $segment - * Controls segment creation. If a falsey value (i.e. `undefined`, `false`, - * `null`, etc) then no segment will be created. If the value is `true`, then - * the name of the current context is used to name the segment. If the value is - * a string then that string will be the name of the segment. Lastly, if the - * value is a function, that function will be called with the current context - * and arguments. - * - * ```js - * var segment = null - * if (spec.$segment) { - * var seg = {name: spec.$segment} - * if (shim.isFunction(seg.name)) { - * seg = seg.name(func, this, arguments) - * } - * else if (seg.name === true) { - * seg.name = func.name - * } - * segment = shim.createSegment(seg.name, seg.recorder, seg.parent) - * } - * ``` - * @property {Object} $cache - * Adds the value as an extra parameter to all specs in the same context as the - * cache. If the current context is a function, the cache will be recreated on - * each invocation of the function. This value can be useful for passing a - * value at runtime from one spec into another. - * - * ```js - * var args = extras || [] - * if (spec.$cache) { - * args.push({}) - * } - * ``` - * @property {number} $callback - * Indicates that one of the parameters is a callback which should be wrapped. - * - * ```js - * if (shim.isNumber(spec.$callback)) { - * var idx = spec.$callback - * if (idx < 0) { - * idx = args.length + idx - * } - * args[idx] = shim.bindSegment(args[idx], segment) - * } - * ``` - * @property {Spec|WrapFunction} property - * Any field which does not start with a `$` is assumed to name a property on - * the current context which should be wrapped. This is simply shorthand for a - * `$wrappings` with only one `$properties` value. - */ - -/** - * @interface SegmentSpec - * @description - * The return value from a {@link SegmentFunction}, used to set the parameters - * of segment creation. - * @property {string} name - * The name for the segment to-be. - * @property {MetricFunction} [recorder] - * A metric recorder for the segment. This is purely for internal use by shim - * classes. Instrumentations should never implement their own metric functions. - * @property {TraceSegment} [parent] - * The parent segment. Defaults to the currently active segment. - * @see RecorderSpec - * @see SegmentFunction - */ - -/** - * @interface RecorderSpec - * @augments SegmentSpec - * @description - * The return value from a {@link RecorderFunction}, used to set the parameters - * of segment creation and lifetime. Extends the {@link SegmentSpec}. - * @property {boolean|string} [stream] - * Indicates if the return value from the wrapped function is a stream. If the - * value is truthy then the recording will extend to the `end` event of the - * stream. If the value is a string it is assumed to be the name of an event to - * measure. A segment will be created to record emissions of the event. - * @property {boolean} [promise] - * Indicates if the return value from the wrapped function is a Promise. If the - * value is truthy then the recording will extend to the completion of the - * Promise. - * @property {number|CallbackBindFunction} [callback] - * If this is a number, it identifies which argument is the callback and the - * segment will also be bound to the callback. Otherwise, the passed function - * should perform the segment binding itself. - * @property {number|CallbackBindFunction} [rowCallback] - * Like `callback`, this identifies a callback function in the arguments. The - * difference is that the default behavior for row callbacks is to only create - * one segment for all calls to the callback. This is mostly useful for - * functions which will be called repeatedly, such as once for each item in a - * result set. - * @property {boolean} [internal=false] - * Marks this as the boundary point into the instrumented library. If `true` - * and the current segment is _also_ marked as `internal` by the same shim, - * then we will not record this inner activity. - * - * This is useful when instrumenting a library which implements high-order - * methods which simply call other public methods and you only want to record - * the method directly called by the user while still instrumenting all - * endpoints. - * @property {Function} [after=null] - * A function to call after the synchronous execution of the recorded method. - * If the function synchronously threw an error, that error will be handed to - * this function. - * @property {boolean} [callbackRequired] - * When `true`, a recorded method must be called with a callback for a segment - * to be created. Does not apply if a custom callback method has been assigned - * via {@link callback}. - * @see SegmentSpec - * @see RecorderFunction - */ - -/** - * @interface ClassWrapSpec - * @description - * Specifies the style of wrapping and construction hooks for wrapping classes. - * @property {boolean} [es6=false] - * Indicates whether the class being wrapped is es6. Our es5 wrapper depends on - * calling the constructor without `new`, so we have to differentiate. - * @property {ConstructorHookFunction} [pre=null] - * A function called with the constructor's arguments before the base class' - * constructor is executed. The `this` value will be `null`. - * @property {ConstructorHookFunction} [post=null] - * A function called with the constructor's arguments after the base class' - * constructor is executed. The `this` value will be the just-constructed object. - */ - // -------------------------------------------------------------------------- // /** @@ -503,7 +293,7 @@ function execute(nodule, spec) { * assumed to be the function to wrap. * @param {Spec|WrapFunction} spec * The spec for wrapping these items. - * @param {Array.<*>} [args=[]] + * @param {Array.<*>} [args] * Optional extra arguments to be sent to the spec when executing it. * @returns {object | Function} The first parameter to this function, after * wrapping it or its properties. @@ -524,9 +314,9 @@ function wrap(nodule, properties, spec, args) { } if (this.isFunction(spec)) { // wrap(nodule [, properties], wrapper [, args]) - spec = { + spec = new specs.WrapSpec({ wrapper: spec - } + }) } // TODO: Add option for omitting symbols.original; unwrappable: false @@ -588,7 +378,7 @@ function wrap(nodule, properties, spec, args) { * assumed to be the function to wrap. * @param {Spec|Function} spec * The spec for wrapping the returned value from the properties. - * @param {Array.<*>} [args=[]] + * @param {Array.<*>} [args] * Optional extra arguments to be sent to the spec when executing it. * @returns {object | Function} The first parameter to this function, after * wrapping it or its properties. @@ -730,7 +520,7 @@ function wrapInProxy({ fn, fnName, shim, args, spec }) { * assumed to be the constructor to wrap. * @param {ClassWrapSpec|ConstructorHookFunction} spec * The spec for wrapping the returned value from the properties or a post hook. - * @param {Array.<*>} [args=[]] + * @param {Array.<*>} [args] * Optional extra arguments to be sent to the spec when executing it. * @returns {object | Function} The first parameter to this function, after * wrapping it or its properties. @@ -745,7 +535,7 @@ function wrapClass(nodule, properties, spec, args) { properties = null } if (this.isFunction(spec)) { - spec = { pre: null, post: spec } + spec = new specs.ClassWrapSpec({ pre: null, post: spec }) } else { spec.pre = spec.pre || null spec.post = spec.post || null @@ -1141,10 +931,10 @@ function getOriginalOnce(nodule, property) { * @param {string} [property] * The property to bind. If omitted, the `nodule` parameter is assumed * to be the function to bind the segment to. - * @param {?TraceSegment} [segment=null] + * @param {?TraceSegment} [segment] * The segment to bind the execution of the function to. If omitted or `null` * the currently active segment will be bound instead. - * @param {boolean} [full=false] + * @param {boolean} [full] * Indicates if the full lifetime of the segment is bound to this function. * @returns {object | Function} The first parameter after wrapping. */ @@ -1398,7 +1188,7 @@ function applySegment(func, segment, full, context, args, inContextCB) { * @memberof Shim.prototype * @param {string} name * The name to give the new segment. - * @param {?Function} [recorder=null] + * @param {?Function} [recorder] * Optional. A function which will record the segment as a metric. Default is * to not record the segment. * @param {TraceSegment} [parent] @@ -1904,7 +1694,7 @@ function assignId(shimName) { * A logical name for the item to be wrapped. * @param {WrapFunction} spec * The spec for wrapping these items. - * @param {Array.<*>} [args=[]] + * @param {Array.<*>} [args] * Optional extra arguments to be sent to the spec when executing it. * @returns {Function} The return value from `spec` or the original value if it * did not return anything. diff --git a/lib/shim/specs/class.js b/lib/shim/specs/class.js new file mode 100644 index 0000000000..070528d64a --- /dev/null +++ b/lib/shim/specs/class.js @@ -0,0 +1,91 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const WrapSpec = require('./wrap') + +/** + * Pre/post constructor execution hook for wrapping classes. + * + * @typedef {function} ConstructorHookFunction + * @param {Shim} shim + * The shim performing the wrapping/binding. + * @param {Function} Base + * The class that was wrapped. + * @param {string} name + * The name of the `Base` class. + * @param {Array.<*>} args + * The arguments to the class constructor. + * @see ClassWrapSpec.pre + */ + +/** + * Spec that provides configuration for wrapping classes (both `class` style + * and traditional `function` style). + */ +class ClassWrapSpec extends WrapSpec { + /** + * When `true`, the class being wrapped is `class` style. Our es5 wrapper + * depends on calling the constructor without `new`, so we have to + * differentiate. + * + * @type {boolean} + */ + es6 + + /** + * When wrapping a `class` based object, the `pre` function will be invoked + * with the class's constructor arguments before the class constructor is + * invoked. The `this` reference will be bound to `null`. + * + * @example + * class Foo { + * constructor(a, b, c) { + * // do stuff + * } + * } + * const spec = new ClassWrapSpec({ + * pre: function (...args) { + * // args = [a, b, c] + * } + * }) + * const wrappedClass = class Wrapper extends Foo { + * constructor() { + * spec.pre.apply(null, [...arguments]) + * } + * } + * + * @see https://github.com/newrelic/node-newrelic/blob/b92ebc0/lib/shim/shim.js#L2005-L2022 + * @type {ConstructorHookFunction|null} + */ + pre + + /** + * As with {@link ClassWrapSpec.pre}, this function will be applied subsequent + * to invoking the wrapped class's constructor. + * + * @see pre + * @type {ConstructorHookFunction|null} + */ + post + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {object} params + * @param {boolean} [params.es6] + * @param {function} [params.pre] + * @param {function} [params.post] + */ + constructor(params) { + super(params) + + this.es6 = params.es6 ?? false + this.pre = params.pre ?? null + this.post = params.post ?? null + } +} + +module.exports = ClassWrapSpec diff --git a/lib/shim/specs/constants.js b/lib/shim/specs/constants.js new file mode 100644 index 0000000000..e64f03b31c --- /dev/null +++ b/lib/shim/specs/constants.js @@ -0,0 +1,16 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const ARG_INDEXES = { + FIRST: 0, + SECOND: 1, + THIRD: 2, + FOURTH: 3, + LAST: -1 +} + +exports.ARG_INDEXES = ARG_INDEXES diff --git a/lib/shim/specs/index.js b/lib/shim/specs/index.js index e79093b7e1..2c30248698 100644 --- a/lib/shim/specs/index.js +++ b/lib/shim/specs/index.js @@ -5,101 +5,32 @@ 'use strict' -const hasOwnProperty = require('../../util/properties').hasOwn -const util = require('util') - -/** - * Enumeration of argument indexes. - * - * Anywhere that an argument index is used, one of these or a direct integer - * value can be used. These are just named constants to improve readability. - * - * Each of these values is also exposed directly on the DatastoreShim class as - * static members. - * - * @readonly - * @memberof Shim.prototype - * @enum {number} - */ -const ARG_INDEXES = { - FIRST: 0, - SECOND: 1, - THIRD: 2, - FOURTH: 3, - LAST: -1 -} - -exports.ARG_INDEXES = ARG_INDEXES - -exports.cast = cast - -exports.MiddlewareSpec = MiddlewareSpec -exports.RecorderSpec = RecorderSpec -exports.SegmentSpec = SegmentSpec -exports.WrapSpec = WrapSpec -exports.MessageSpec = MessageSpec -exports.MessageSubscribeSpec = MessageSubscribeSpec - -function cast(Class, spec) { - return spec instanceof Class ? spec : new Class(spec) -} - -function WrapSpec(spec) { - this.wrapper = typeof spec === 'function' ? spec : spec.wrapper - this.matchArity = hasOwnProperty(spec, 'matchArity') ? spec.matchArity : false -} - -function SegmentSpec(spec) { - this.name = hasOwnProperty(spec, 'name') ? spec.name : null - this.recorder = hasOwnProperty(spec, 'recorder') ? spec.recorder : null - this.inContext = hasOwnProperty(spec, 'inContext') ? spec.inContext : null - this.parent = hasOwnProperty(spec, 'parent') ? spec.parent : null - this.parameters = hasOwnProperty(spec, 'parameters') ? spec.parameters : null - this.internal = hasOwnProperty(spec, 'internal') ? spec.internal : false - this.opaque = hasOwnProperty(spec, 'opaque') ? spec.opaque : false -} - -function RecorderSpec(spec) { - SegmentSpec.call(this, spec) - this.stream = hasOwnProperty(spec, 'stream') ? spec.stream : null - this.promise = hasOwnProperty(spec, 'promise') ? spec.promise : null - this.callback = hasOwnProperty(spec, 'callback') ? spec.callback : null - this.rowCallback = hasOwnProperty(spec, 'rowCallback') ? spec.rowCallback : null - this.after = hasOwnProperty(spec, 'after') ? spec.after : null - this.callbackRequired = hasOwnProperty(spec, 'callbackRequired') ? spec.callbackRequired : null -} -util.inherits(RecorderSpec, SegmentSpec) - -function MiddlewareSpec(spec) { - RecorderSpec.call(this, spec) - this.req = hasOwnProperty(spec, 'req') ? spec.req : ARG_INDEXES.FIRST - this.res = hasOwnProperty(spec, 'res') ? spec.res : ARG_INDEXES.SECOND - this.next = hasOwnProperty(spec, 'next') ? spec.next : ARG_INDEXES.THIRD - this.type = hasOwnProperty(spec, 'type') ? spec.type : 'MIDDLEWARE' - this.route = hasOwnProperty(spec, 'route') ? spec.route : null - this.params = hasOwnProperty(spec, 'params') ? spec.params : _defaultGetParams - this.appendPath = hasOwnProperty(spec, 'appendPath') ? spec.appendPath : true -} -util.inherits(MiddlewareSpec, RecorderSpec) - -function MessageSpec(spec) { - RecorderSpec.call(this, spec) - this.destinationName = hasOwnProperty(spec, 'destinationName') ? spec.destinationName : null - this.destinationType = hasOwnProperty(spec, 'destinationType') ? spec.destinationType : null - this.headers = hasOwnProperty(spec, 'headers') ? spec.headers : null - this.routingKey = hasOwnProperty(spec, 'routingKey') ? spec.routingKey : null - this.queue = hasOwnProperty(spec, 'queue') ? spec.queue : null - this.messageHandler = hasOwnProperty(spec, 'messageHandler') ? spec.messageHandler : null -} - -util.inherits(MessageSpec, RecorderSpec) - -function MessageSubscribeSpec(spec) { - MessageSpec.call(this, spec) - this.consumer = hasOwnProperty(spec, 'consumer') ? spec.consumer : null -} -util.inherits(MessageSubscribeSpec, MessageSpec) - -function _defaultGetParams(shim, fn, name, args, req) { - return req && req.params +const { ARG_INDEXES } = require('./constants') +const ClassWrapSpec = require('./class') +const MessageSpec = require('./message') +const MessageSubscribeSpec = require('./message-subscribe') +const MiddlewareSpec = require('./middleware') +const MiddlewareMounterSpec = require('./middleware-mounter') +const OperationSpec = require('./operation') +const QuerySpec = require('./query') +const RecorderSpec = require('./recorder') +const RenderSpec = require('./render') +const SegmentSpec = require('./segment') +const TransactionSpec = require('./transaction') +const WrapSpec = require('./wrap') + +module.exports = { + ARG_INDEXES, + ClassWrapSpec, + MessageSpec, + MessageSubscribeSpec, + MiddlewareSpec, + MiddlewareMounterSpec, + OperationSpec, + QuerySpec, + RecorderSpec, + RenderSpec, + SegmentSpec, + TransactionSpec, + WrapSpec } diff --git a/lib/shim/specs/message-subscribe.js b/lib/shim/specs/message-subscribe.js new file mode 100644 index 0000000000..94b9de25a8 --- /dev/null +++ b/lib/shim/specs/message-subscribe.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const MessageSpec = require('./message') + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} MessageSubscribeSpecParams + * @augments MessageSpecParams + * @property {number|null} [consumer] + */ + +/** + * Spec to describe instrumenting a message broker consumer entity, i.e. + * the thing that reads and processes messages from a message broker. + */ +class MessageSubscribeSpec extends MessageSpec { + /** + * Indicates the position in the instrumented consumer function's arguments + * list that represents the thing that will handle messages. + * + * @type {number|null} + */ + consumer + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {MessageSubscribeSpecParams} params + */ + constructor(params) { + super(params) + + this.consumer = params.consumer ?? null + } +} + +module.exports = MessageSubscribeSpec diff --git a/lib/shim/specs/message.js b/lib/shim/specs/message.js new file mode 100644 index 0000000000..7673b1a3b1 --- /dev/null +++ b/lib/shim/specs/message.js @@ -0,0 +1,90 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const RecorderSpec = require('./recorder') + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} MessageSpecParams + * @augments RecorderSpecParams + * @property {number|string} [destinationName] + * @property {string|null} [destinationType] + * @property {Object|null} [headers] + * @property {MessageHandlerFunction|null} [messageHandler] + * @property {number|string|null} [queue] + * @property {string|null} [routingKey] + */ + +/** + * Spec that describes how to instrument a message broker. + */ +class MessageSpec extends RecorderSpec { + /** + * If a number, then it indicates the argument position of the name in the + * instrumented function's parameters list. Otherwise, it is a string name. + * + * @type {number|string|null} + */ + destinationName + + /** + * Label indicating what type of message broker is being instrumented. + * + * @see {MessageShimTypes} + * @type {string|null} + */ + destinationType + + /** + * Headers to insert into the request being instrumented. + * + * @type {Object|null} + */ + headers + + /** + * A function to handle the result of the instrumented message broker + * function. + * + * @type {MessageHandlerFunction} + */ + messageHandler + + /** + * When a number, indicates the argument position of the message queue in the + * instrumented function's arguments list. Otherwise, it is a string + * representing the name of the queue. + * + * @type {number|string|null} + */ + queue + + /** + * The name of the key that provides the routing path for the message + * broker. + * + * @type {string|null} + */ + routingKey + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {MessageSpecParams} params + */ + constructor(params) { + super(params) + + this.destinationName = params.destinationName ?? null + this.destinationType = params.destinationType ?? null + this.headers = params.headers ?? null + this.messageHandler = params.messageHandler ?? null + this.queue = params.queue ?? null + this.routingKey = params.routingKey ?? null + } +} + +module.exports = MessageSpec diff --git a/lib/shim/specs/middleware-mounter.js b/lib/shim/specs/middleware-mounter.js new file mode 100644 index 0000000000..c4ab5f8e73 --- /dev/null +++ b/lib/shim/specs/middleware-mounter.js @@ -0,0 +1,77 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const MiddlewareSpec = require('./middleware') + +/** + * Called whenever new middleware are mounted using the instrumented framework, + * this method should pull out a representation of the mounted path. + * + * @typedef {function} RouteParserFunction + * @param {WebFrameworkShim} shim + * The shim in use for this instrumentation. + * @param {Function} fn + * The function which received this route string/RegExp. + * @param {string} fnName + * The name of the function to which this route was given. + * @param {string|RegExp} route + * The route that was given to the function. + * @returns {string|RegExp} The mount point from the given route. + */ + +/** + * Called for each middleware passed to a mounting method. Should perform the + * wrapping of the middleware. + * + * @typedef {function} MiddlewareWrapperFunction + * @param {WebFrameworkShim} shim + * The shim used for instrumentation. + * @param {Function} middleware + * The middleware function to wrap. + * @param {string} fnName + * The name of the middleware function. + * @param {string} [route=null] + * The route the middleware is mounted on if one was found. + * @see WebFrameworkShim#recordMiddleware + * @see WebFrameworkShim#recordParamware + */ + +class MiddlewareMounterSpec extends MiddlewareSpec { + /** + * Indicates which argument specifies the mounting path for the other + * arguments in a middleware mounting method's arguments list. When set to + * a function, it is assumed the route was not provided and the indicated + * argument is a middleware function. If a string is provided, it will be + * used as the mounting path. If a number is provided, then it indicates + * the position in the arguments list that represents the route. + * + * @type {RouteParserFunction|string|number} + */ + route + + /** + * A function to invoke for each middleware function passed to the mounter. + * + * @type {MiddlewareWrapperFunction} + */ + wrapper + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {object} params + * @param {RouteParserFunction|string|number|null} [params.route] + * @param {MiddlewareWrapperFunction} [params.wrapper] + */ + constructor(params) { + super(params) + + this.route = params.route ?? null + this.wrapper = params.wrapper ?? null + } +} + +module.exports = MiddlewareMounterSpec diff --git a/lib/shim/specs/middleware.js b/lib/shim/specs/middleware.js new file mode 100644 index 0000000000..a3112327fe --- /dev/null +++ b/lib/shim/specs/middleware.js @@ -0,0 +1,149 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const { ARG_INDEXES } = require('./constants') +const RecorderSpec = require('./recorder') + +/** + * Extracts the request object from the arguments to the middleware function. + * + * @typedef {function} RouteRequestFunction + * @param {WebFrameworkShim} shim The shim used for instrumentation. + * @param {Function} fn The middleware function. + * @param {string} fnName The name of the middleware function. + * @param {Array} args The arguments to the middleware function. + * @returns {object} The request object. + */ + +/** + * Used to wrap functions that users can call to continue to the next + * middleware. + * + * @typedef {function} RouteNextFunction + * @param {WebFrameworkShim} shim The shim used for instrumentation. + * @param {Function} fn The middleware function. + * @param {string} fnName The name of the middleware function. + * @param {Array} args The arguments to the middleware function. + * @returns {object} The request object. + */ + +/** + * Extracts the route parameters from the arguments to the middleware function. + * + * @typedef {function} RouteParameterFunction + * @param {WebFrameworkShim} shim The shim used for instrumentation. + * @param {Function} fn The middleware function. + * @param {string} fnName The name of the middleware function. + * @param {Array} args The arguments to the middleware function. + * @returns {object} A map of route parameter names to values. + */ + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} MiddlewareSpecParams + * @augments RecorderSpecParams + * @property {boolean} [appendPath] + * @property {number|RouteNextFunction} [next] + * @property {RouteParameterFunction|null} [params] + * @property {number|RouteRequestFunction} [req] + * @property {number} [res] + * @property {number|string|null} [route] + * @property {string} [type] + */ + +/** + * Spec that describes how to instrument a framework middleware function, e.g. + * an `express` middleware. + */ +class MiddlewareSpec extends RecorderSpec { + /** + * Indicates if the route path for the middleware should be appended to + * the transaction name or not. + * + * @type {boolean} + */ + appendPath + + /** + * When a number, indicates the argument position of the "next" callback in + * the original middleware function's parameters list. Otherwise, it's a + * function that will be invoked with the arguments of the middleware and + * another function for wrapping calls that represent continuation from the + * instrumented middleware. + * + * @type {number|RouteNextFunction} + */ + next + + /** + * A function to extract the route parameters from the instrumented + * middleware's arguments list. + * + * @type {RouteParameterFunction} + */ + params + + /** + * When a number, indicates the argument position of the request object in + * the middleware function's arguments list. Otherwise, it's a function that + * extracts the request object from the middleware arguments. + * + * @type {number|RouteRequestFunction} + */ + req + + /** + * Indicates the argument position of the response object in the middleware + * function's arguments list. + * + * @type {number} + */ + res + + /** + * When a number, indicates the argument position of the route string in the + * middleware function's arguments list. Otherwise, it is a string that + * represents the route path. + * + * @type {number|string|null} + */ + route + + /** + * Indicates the type of middleware that is being instrumented. + * + * @see {MiddlewareTypeNames} + * @type {string} + */ + type + + /** + * @param {MiddlewareSpecParams} constructorParams + */ + constructor(constructorParams) { + super(constructorParams) + + this.appendPath = constructorParams.appendPath ?? true + this.next = constructorParams.next ?? ARG_INDEXES.THIRD + this.params = + constructorParams.params ?? + function getParamsFromReq(...args) { + // At some point in the future, after more inspection and wrapping + // has been done, this function will be invoked with a potential + // `req` object as the last parameters. + // See https://github.com/newrelic/node-newrelic/blob/f33c0cc/lib/shim/webframework-shim/middleware.js#L69 + const req = args.at(-1) + return req && req.params + } + this.req = constructorParams.req ?? ARG_INDEXES.FIRST + this.res = constructorParams.res ?? ARG_INDEXES.SECOND + this.route = constructorParams.route ?? null + this.type = constructorParams.type ?? 'MIDDLEWARE' + } +} + +module.exports = MiddlewareSpec diff --git a/lib/shim/specs/operation.js b/lib/shim/specs/operation.js new file mode 100644 index 0000000000..5a7b74d9f5 --- /dev/null +++ b/lib/shim/specs/operation.js @@ -0,0 +1,59 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const RecorderSpec = require('./recorder') + +/** + * Extra parameters which may be added to an operation or query segment. All of + * these properties are optional. + * + * @typedef {object} DatastoreParameters + * @property {string} host + * The host of the database server being interacted with. If provided, along + * with `port_path_or_id`, then an instance metric will also be generated for + * this database. + * @property {number|string} port_path_or_id + * The port number or path to domain socket used to connect to the database + * server. + * @property {string} database_name + * The name of the database being queried or operated on. + */ + +/** + * Spec that describes an operation, e.g. connecting to a database. + */ +class OperationSpec extends RecorderSpec { + /** + * Extra parameters to be set on the metric for the operation. + * + * @type {DatastoreParameters|null} + */ + parameters + + /** + * Indicates if the operation should be recorded as a metric. A segment will + * be created even if this is `false`. + * + * @type {boolean} + */ + record + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {object} params + * @param {DatastoreParameters|null} [params.parameters] + * @param {boolean} [params.record] + */ + constructor(params) { + super(params) + + this.parameters = params.parameters ?? null + this.record = params.record ?? true + } +} + +module.exports = OperationSpec diff --git a/lib/shim/specs/query.js b/lib/shim/specs/query.js new file mode 100644 index 0000000000..0d97f54a4e --- /dev/null +++ b/lib/shim/specs/query.js @@ -0,0 +1,47 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const OperationSpec = require('./operation') + +/** + * Retrieves the query argument from an array of arguments. + * + * @typedef {function} QueryFunction + * @param {Shim} shim The shim this function was passed to. + * @param {Function} func The function being recorded. + * @param {string} name The name of the function. + * @param {Array.<*>} args The arguments being passed into the function. + * @returns {string} The query string from the arguments list. + */ + +/** + * Spec that describes a database query operation. + */ +class QuerySpec extends OperationSpec { + /** + * When set to a number it represents the position in the function's + * arguments that is the query string. If a string, it is the query to be + * executed. Otherwise, if it is a function, it will be passed the + * arguments and must return the query string. + * + * @type {number|string|QueryFunction} + */ + query + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {object} params + * @param {number|string|QueryFunction} [params.query] + */ + constructor(params) { + super(params) + + this.query = params.query ?? null + } +} + +module.exports = QuerySpec diff --git a/lib/shim/specs/recorder.js b/lib/shim/specs/recorder.js new file mode 100644 index 0000000000..f72078ef4d --- /dev/null +++ b/lib/shim/specs/recorder.js @@ -0,0 +1,123 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const WrapSpec = require('./wrap') + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} RecorderSpecParams + * @augments SegmentSpecParams + * @property {SpecAfterFunction} [after] + * @property {number|CallbackBindFunction} [callback] + * @property {boolean} [callbackRequired] + * @property {boolean} [promise] + * @property {number|CallbackBindFunction} [rowCallback] + * @property {boolean|string} [stream] + */ + +/** + * A callback invoked after an instrumented function has completed its work. + * The instrumented function must have been invoked synchronously. + * + * @typedef {Function} SpecAfterFunction + * @param {object} shim The shim used to instrument the external library. + * @param {Function} fn The function/method from the external library being + * instrumented. + * @param {string} name The name of the current function. + * @param {Error|null} error If the instrumented function threw an error, this + * will be that error. + * @param {*} value The result returned by the instrumented function. + * @param {TraceSegment} segment The segment used while instrumenting the + * function. + */ + +/** + * A specialized case of {@link SegmentSpec}. A `RecorderSpec` is typically + * used with {@link Shim.record}. It defines the parameters of segment creation + * and segment lifetime. + */ +class RecorderSpec extends WrapSpec { + /** + * @type {SpecAfterFunction} + */ + after + + /** + * If a number, then the number indicates the position in the instrumented + * function's arguments list that represents the callback function. Otherwise, + * it should be the function to used in conjunction with the instrumented + * function. + * + * @example Using a number + * const spec = new RecorderSpec({ callback: -1 }) + * // elsewhere + * const cb = Array.from(arguments).at(spec.callback) + * + * @example Using a function + * const spec = new RecorderSpec({ callback: () => { + * console.log('hello') + * }) + * // elsewhere + * instrumentedFunction('foo', spec.callback) + * + * @type {number|CallbackBindFunction} + */ + callback + + /** + * When `true`, a recorded method must be called with a callback for a segment + * to be created. Does not apply if a custom callback method has been + * assigned via {@link callback}. + * + * @type {boolean} + */ + callbackRequired + + /** + * Indicates if the instrumented function is expected to return a promise. + * When `true`, the segment recording will be extended until the promise + * has settled. + * + * @type {boolean|null} + */ + promise + + /** + * Like {@link callback}, this identifies a callback function in the + * instrumented function's arguments list. The difference is that the default + * behavior for row callbacks is to only create one segment for all calls to + * the callback. This is mostly useful for functions which will be called + * repeatedly, such as once for each item in a result set. + * + * @type {number|CallbackBindFunction} + */ + rowCallback + + /** + * Indicates if the instrumented function is expected to return a stream. + * When `true`, the segment recording will be extended until the `end` event + * of the stream. If the value is a string, it is assumed to be the name of + * an event to measure; a segment will be created to record emissions of the + * named event. + * + * @type {boolean|string|null} + */ + stream + + constructor(params) { + super(params) + + this.after = params.after ?? null + this.callback = params.callback ?? null + this.callbackRequired = params.callbackRequired ?? null + this.promise = params.promise ?? null + this.rowCallback = params.rowCallback ?? null + this.stream = params.stream ?? null + } +} + +module.exports = RecorderSpec diff --git a/lib/shim/specs/render.js b/lib/shim/specs/render.js new file mode 100644 index 0000000000..6aa599150b --- /dev/null +++ b/lib/shim/specs/render.js @@ -0,0 +1,42 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const RecorderSpec = require('./recorder') +const { ARG_INDEXES } = require('./constants') + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} RecorderSpecParams + * @augments RecorderSpecParams + * @property {number} [view] + */ + +/** + * Spec describing how to wrap a view middleware. + * + * @see https://github.com/newrelic/node-newrelic/blob/cde1014e/lib/shim/webframework-shim/index.js#L301-L333 + */ +class RenderSpec extends RecorderSpec { + /** + * Identifies the position of the view name argument in the instrumented + * view middleware's arguments list. + * + * @type {number} + */ + view + + /** + * @param {RecorderSpecParams} params + */ + constructor(params) { + super(params) + + this.view = params.view ?? ARG_INDEXES.FIRST + } +} + +module.exports = RenderSpec diff --git a/lib/shim/specs/segment.js b/lib/shim/specs/segment.js new file mode 100644 index 0000000000..64ece844d2 --- /dev/null +++ b/lib/shim/specs/segment.js @@ -0,0 +1,114 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const Spec = require('./spec') + +/** + * A function that will be invoked in the context of the current segment. + * Instrumentations that need to perform operations during the invocation of + * a method that has been instrumented can provide an `InContextCallback` + * function to accomplish their needs. The callback is invoked in the same + * async context as the instrumented function, i.e. concurrent to the execution + * of the instrumented function, and within the same segment. + * + * @typedef {Function} InContextCallback + * @param {TraceSegment} segment The current segment. + */ + +/* eslint-disable jsdoc/require-property-description */ +/** + * @typedef {object} SegmentSpecParams + * @property {InContextCallback} [inContext] + * @property {boolean} [internal] + * @property {string} [name] + * @property {boolean} [opaque] + * @property {Object} [parameters] + * @property {TraceSegment} [parent] + * @property {MetricFunction} [recorder] + */ + +/** + * Baseline spec implementation. Can be utilized anywhere a generic spec will + * satisfy the requirements. Some shims require more specialized specs that + * this one does not satisfy. + */ +class SegmentSpec extends Spec { + /** + * @type {InContextCallback} + */ + inContext + + /** + * Marks the segment as the boundary point into an instrumented library. If + * set to `true`, and the parent segment is _also_ marked as `internal: true` + * by the same shim, then we will not record this inner activity. + * + * This is useful when instrumenting a library which implements high-order + * methods which simply call other public method, and you only want to + * record the method directly called by the user while still instrumenting + * all endpoints. + * + * @type {boolean} + */ + internal + + /** + * A name for the segment that can be recognized by users. + * + * @type {string} + */ + name + + /** + * Indicates if child segments should be recorded or not. When `true`, child + * segments will not be created and traces will omit the details descended + * from an opaque segment. + * + * @type {boolean} + */ + opaque + + /** + * A key-value hash of attributes that a shim can utilize. + * + * @type {Object} + */ + parameters + + /** + * The parent segment, if any. Should be set to the currently active + * segment by default. + * + * @type {TraceSegment} + */ + parent + + /** + * A metric recorder for the segment. This field is intended to be used by + * shim implementations. It is not intended that an instrumentation will + * ever need to specify its own metric recorder. + * + * @type {MetricFunction} + */ + recorder + + /** + * @param {SegmentSpecParams} params Spec properties to set. + */ + constructor(params) { + super() + this.inContext = params.inContext ?? null + this.internal = params.internal ?? false + this.name = params.name ?? null + this.opaque = params.opaque ?? false + this.parameters = params.parameters ?? null + this.parent = params.parent ?? null + this.recorder = params.recorder ?? null + } +} + +module.exports = SegmentSpec diff --git a/lib/shim/specs/spec.js b/lib/shim/specs/spec.js new file mode 100644 index 0000000000..2f1f8c8032 --- /dev/null +++ b/lib/shim/specs/spec.js @@ -0,0 +1,18 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +/** + * A spec is utilized by shims as a method of describing how an instrumentation + * should be performed. They provide a convenient object to hang custom + * properties on in addition to the set of well-known fields and methods. + * + * @private + * @interface + */ +class Spec {} + +module.exports = Spec diff --git a/lib/shim/specs/transaction.js b/lib/shim/specs/transaction.js new file mode 100644 index 0000000000..f41b8195e5 --- /dev/null +++ b/lib/shim/specs/transaction.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const SegmentSpec = require('./segment') + +/** + * Spec that describes the type of agent transaction to be created by the + * function being wrapped by {@link TransactionShim.bindCreateTransaction}. + */ +class TransactionSpec extends SegmentSpec { + /** + * Indicates if the transaction being created is allowed to be nested within + * another transaction of the same type. If `false`, the default, the + * transaction will only be created if there is no existing transaction, or + * the current transaction is of a different type. If `true`, the transaction + * will be created regardless of the current transaction's type. + * + * @type {boolean} + */ + nest + + /** + * The type of the transaction to create. Must be one of the values from + * {@link TransactionShim.TRANSACTION_TYPES}. + * + * @type {string} + */ + type + + /* eslint-disable jsdoc/require-param-description */ + /** + * @param {object} params + * @param {boolean} [params.nest] + * @param {string} [params.type] + */ + constructor(params) { + super(params) + + this.nest = params.nest ?? false + + if (typeof params.type !== 'string') { + throw Error('params.type must be a string') + } + this.type = params.type + } +} + +module.exports = TransactionSpec diff --git a/lib/shim/specs/wrap.js b/lib/shim/specs/wrap.js new file mode 100644 index 0000000000..4cab1a0307 --- /dev/null +++ b/lib/shim/specs/wrap.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const SegmentSpec = require('./segment') + +/** + * Provides configuration for wrapping functions. + * + * @example + * function toWrap (a, b) {} + * const wrapped = shim.wrap(toWrap, { + * matchArity: true, + * wrapper: function () { + * return function wrappedFn () {} + * } + * }) + * assert.equal(toWrap.length, wrapped.length) + */ +class WrapSpec extends SegmentSpec { + /** + * Indicates that the arity of the wrapper should match the arity of the + * function being wrapped. + * + * @type {boolean} + */ + matchArity + + /** + * A function that wraps another function. + * + * @type {function} + */ + wrapper + + /* eslint-disable jsdoc/require-param-description */ + /** + * The parameters may be a function; if so, that function is used as the + * wrapper. Otherwise, the parameters must be an object with a `wrapper` + * property set to the function that should be the wrapper. + * + * @param {object|function} params + * @param {boolean} [params.matchArity] + * @param {function} [params.wrapper] + */ + constructor(params) { + super(params) + + this.matchArity = params.matchArity ?? false + if (typeof params === 'function') { + this.wrapper = params + } else { + this.wrapper = params.wrapper + } + } +} + +module.exports = WrapSpec diff --git a/lib/shim/transaction-shim.js b/lib/shim/transaction-shim.js index 8c5aab3284..d54a8b2341 100644 --- a/lib/shim/transaction-shim.js +++ b/lib/shim/transaction-shim.js @@ -72,28 +72,6 @@ TransactionShim.prototype.handleMqTracingHeaders = handleMqTracingHeaders TransactionShim.prototype.insertCATReplyHeader = insertCATReplyHeader TransactionShim.prototype.insertCATRequestHeaders = insertCATRequestHeaders -// -------------------------------------------------------------------------- // - -/** - * @interface TransactionSpec - * @description - * Describes the type of transaction to be created by the function being - * wrapped by {@link Shim#bindCreateTransaction}. - * @property {string} type - * The type of transaction to create. Must be one of the values from - * {@link Shim#TRANSACTION_TYPES}. - * @property {boolean} [nest=false] - * Indicates if the transaction being created is allowed to be nested within - * another transaction of the same type. If `false`, the default, the transaction - * will only be created if there is no existing transaction, or the current - * transaction is of a different type. If `true`, the transaction will be - * created regardless of the current transaction's type. - * @see Shim#bindCreateTransaction - * @see Shim#TRANSACTION_TYPES - */ - -// -------------------------------------------------------------------------- // - /** * Wraps one or more functions such that new transactions are created when * invoked. @@ -214,10 +192,10 @@ function setTransactionName(name) { * CAT headers. * @param {object} headers * The request/response headers object to look in. - * @param {TraceSegment} [segment=null] + * @param {TraceSegment} [segment] * The trace segment to associate the header data with. If no segment is * provided then the currently active segment is used. - * @param {string} [transportType='Unknown'] + * @param {string} [transportType] * The transport type that brought the headers. Usually `HTTP` or `HTTPS`. */ function handleMqTracingHeaders(headers, segment, transportType) { @@ -271,7 +249,7 @@ function handleMqTracingHeaders(headers, segment, transportType) { * @memberof TransactionShim.prototype * @param {object} headers * The outbound request headers object to inject our CAT headers into. - * @param {boolean} [useAlternateHeaderNames=false] + * @param {boolean} [useAlternateHeaderNames] * Indicates if HTTP-style headers should be used or alternate style. Some * transport protocols are more strict on the characters allowed in headers * and this option can be used to toggle use of pure-alpha header names. @@ -314,7 +292,7 @@ function insertCATRequestHeaders(headers, useAlternateHeaderNames) { * @memberof TransactionShim.prototype * @param {object} headers * The outbound response headers object to inject our CAT headers into. - * @param {boolean} [useAlternateHeaderNames=false] + * @param {boolean} [useAlternateHeaderNames] * Indicates if HTTP-style headers should be used or alternate style. Some * transport protocols are more strict on the characters allowed in headers * and this option can be used to toggle use of pure-alpha header names. diff --git a/lib/shim/webframework-shim/common.js b/lib/shim/webframework-shim/common.js index 91a4c44f6d..b11cc46cc3 100644 --- a/lib/shim/webframework-shim/common.js +++ b/lib/shim/webframework-shim/common.js @@ -8,6 +8,9 @@ const symbols = require('../../symbols') const common = module.exports +/** + * @typedef {Object} MiddlewareTypeNames + */ common.MIDDLEWARE_TYPE_NAMES = { APPLICATION: 'APPLICATION', ERRORWARE: 'ERRORWARE', diff --git a/lib/shim/webframework-shim/index.js b/lib/shim/webframework-shim/index.js index c2352cdfb5..4944535414 100644 --- a/lib/shim/webframework-shim/index.js +++ b/lib/shim/webframework-shim/index.js @@ -102,128 +102,6 @@ WebFrameworkShim.prototype.setErrorPredicate = setErrorPredicate WebFrameworkShim.prototype.setResponsePredicate = setResponsePredicate WebFrameworkShim.prototype.savePossibleTransactionName = savePossibleTransactionName -// -------------------------------------------------------------------------- // - -/** - * @callback RouteParserFunction - * @summary - * Called whenever new middleware are mounted using the instrumented framework, - * this method should pull out a representation of the mounted path. - * @param {WebFrameworkShim} shim - * The shim in use for this instrumentation. - * @param {Function} fn - * The function which received this route string/RegExp. - * @param {string} fnName - * The name of the function to which this route was given. - * @param {string|RegExp} route - * The route that was given to the function. - * @returns {string|RegExp} The mount point from the given route. - */ - -/** - * @callback RouteRequestFunction - * @summary - * Extracts the request object from the arguments to the middleware function. - * @param {WebFrameworkShim} shim - The shim used for instrumentation. - * @param {Function} fn - The middleware function. - * @param {string} fnName - The name of the middleware function. - * @param {Array} args - The arguments to the middleware function. - * @returns {object} The request object. - */ - -/** - * @callback RouteNextFunction - * @summary - * Used to wrap functions that users can call to continue to the next middleware. - * @param {WebFrameworkShim} shim - The shim used for instrumentation. - * @param {Function} fn - The middleware function. - * @param {string} fnName - The name of the middleware function. - * @param {Array} args - The arguments to the middleware function. - * @returns {object} The request object. - */ - -/** - * @callback RouteParameterFunction - * @summary - * Extracts the route parameters from the arguments to the middleware function. - * @param {WebFrameworkShim} shim - The shim used for instrumentation. - * @param {Function} fn - The middleware function. - * @param {string} fnName - The name of the middleware function. - * @param {Array} args - The arguments to the middleware function. - * @returns {object} A map of route parameter names to values. - */ - -/** - * @callback MiddlewareWrapperFunction - * @summary - * Called for each middleware passed to a mounting method. Should perform the - * wrapping of the middleware. - * @param {WebFrameworkShim} shim - * The shim used for instrumentation. - * @param {Function} middleware - * The middleware function to wrap. - * @param {string} fnName - * The name of the middleware function. - * @param {string} [route=null] - * The route the middleware is mounted on if one was found. - * @see WebFrameworkShim#recordMiddleware - * @see WebFrameworkShim#recordParamware - */ - -/** - * @interface MiddlewareSpec - * @description - * Describes the interface for middleware functions with this instrumentation. - * @property {number|RouteRequestFunction} [req=shim.FIRST] - * Indicates which argument to the middleware is the request object. It can also be - * a function to extract the request object from the middleware arguments. - * @property {number} [res=shim.SECOND] - * Indicates which argument to the middleware is the response object. - * @property {number|RouteNextFunction} [next=shim.THIRD] - * Indicates which argument to the middleware function is the callback. When it is - * a function, it will be called with the arguments of the middleware and a function - * for wrapping calls that represent continuation from the current middleware. - * @property {string} [name] - * The name to use for this middleware. Defaults to `middleware.name`. - * @property {RouteParameterFunction} [params] - * A function to extract the route parameters from the middleware arguments. - * Defaults to using `req.params`. - * @property {string} [type='MIDDLEWARE'] specifies the type - * @property {string | Function} [route=null] - * Route/path used for naming segments and transaction name candidates. If a function, - * will be invoked just before segment creation with middleware invocation. - * @property {boolean} [appendPath=true] - * Indicates that the path associated with the middleware should be appended - * and popped from the stack of name candidates. - */ - -/** - * @interface MiddlewareMounterSpec - * @description - * Describes the arguments provided to mounting methods (e.g. `app.post()`). - * @property {number|string} [route=null] - * Tells which argument may be the mounting path for the other arguments. If - * the indicated argument is a function it is assumed the route was not provided - * and the indicated argument is a middleware function. If a string is provided - * it will be used as the mounting path. - * @property {MiddlewareWrapperFunction} [wrapper] - * A function to call for each middleware function passed to the mounter. - */ - -/** - * @interface RenderSpec - * @augments RecorderSpec - * @description - * Describes the interface for render methods. - * @property {number} [view=shim.FIRST] - * Identifies which argument is the name of the view being rendered. Defaults - * to {@link Shim#ARG_INDEXES shim.FIRST}. - * @see SegmentSpec - * @see RecorderSpec - */ - -// -------------------------------------------------------------------------- // - /** * Sets the function used to convert the route handed to middleware-adding * methods into a string. @@ -305,11 +183,14 @@ function recordRender(nodule, properties, spec) { properties = null } - spec = this.setDefaults(spec, { - view: this.FIRST, - callback: null, - promise: null - }) + spec = this.setDefaults( + spec, + new specs.RenderSpec({ + view: this.FIRST, + callback: null, + promise: null + }) + ) return this.record(nodule, properties, function renderRecorder(shim, fn, name, args) { const viewIdx = shim.normalizeIndex(args.length, spec.view) diff --git a/lib/shim/webframework-shim/middleware-mounter.js b/lib/shim/webframework-shim/middleware-mounter.js index c55b344fae..dbec41e085 100644 --- a/lib/shim/webframework-shim/middleware-mounter.js +++ b/lib/shim/webframework-shim/middleware-mounter.js @@ -4,7 +4,9 @@ */ 'use strict' + const { copyExpectedSpecParameters } = require('./common') +const { MiddlewareMounterSpec } = require('../specs') /** * Wraps route mounter and all middleware defined within mounter(arguments to mounter) @@ -108,7 +110,7 @@ module.exports = function wrapMiddlewareMounter(nodule, properties, spec) { } if (this.isFunction(spec)) { // wrapMiddlewareMounter(nodule [, properties], wrapper) - spec = { wrapper: spec } + spec = new MiddlewareMounterSpec({ wrapper: spec }) } spec = this.setDefaults(spec, { @@ -116,9 +118,9 @@ module.exports = function wrapMiddlewareMounter(nodule, properties, spec) { endpoint: null }) - const wrapSpec = { + const wrapSpec = new MiddlewareMounterSpec({ wrapper: wrapMounter.bind(null, spec) - } + }) copyExpectedSpecParameters(wrapSpec, spec) diff --git a/lib/transaction/trace/index.js b/lib/transaction/trace/index.js index 94748362f2..83cb05f86e 100644 --- a/lib/transaction/trace/index.js +++ b/lib/transaction/trace/index.js @@ -148,7 +148,7 @@ function DTTraceNode(segment, parentId, isRoot = false) { * * @param {string} childName Name for the new segment. * @param {Function} callback Callback function to record metrics related to the trace - * @returns {Segment} Newly-created Segment. + * @returns {TraceSegment} Newly-created segment. */ Trace.prototype.add = function add(childName, callback) { return this.root.add(childName, callback) diff --git a/lib/util/properties.js b/lib/util/properties.js index 8dcf5e0549..0543038a4c 100644 --- a/lib/util/properties.js +++ b/lib/util/properties.js @@ -5,7 +5,7 @@ 'use strict' -const hasOwnProperty = Object.hasOwnProperty +const hasOwnProperty = Object.prototype.hasOwnProperty /** * Checks if an object has its own property with the given key. diff --git a/package-lock.json b/package-lock.json index a0da165561..ed5900c56d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "conventional-commits-parser": "^3.2.4", "eslint": "^8.24.0", "eslint-plugin-disable": "^2.0.1", - "eslint-plugin-jsdoc": "^39.3.6", + "eslint-plugin-jsdoc": "^48.0.5", "eslint-plugin-sonarjs": "^0.18.0", "express": "*", "git-raw-commits": "^2.0.11", @@ -521,17 +521,17 @@ } }, "node_modules/@es-joy/jsdoccomment": { - "version": "0.36.1", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz", - "integrity": "sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.42.0.tgz", + "integrity": "sha512-R1w57YlVA6+YE01wch3GPYn6bCsrOV3YW/5oGGE2tmX6JcL9Nr+b5IikrjMPF+v9CV3ay+obImEdsDhovhJrzw==", "dev": true, "dependencies": { - "comment-parser": "1.3.1", - "esquery": "^1.4.0", - "jsdoc-type-pratt-parser": "~3.1.0" + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18 || ^19" + "node": ">=16" } }, "node_modules/@eslint-community/eslint-utils": { @@ -4130,6 +4130,15 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "dev": true }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -4563,6 +4572,18 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -5202,9 +5223,9 @@ } }, "node_modules/comment-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", - "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, "engines": { "node": ">= 12.0.0" @@ -6042,24 +6063,26 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "39.9.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz", - "integrity": "sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw==", + "version": "48.0.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-48.0.6.tgz", + "integrity": "sha512-LgwXOX6TWxxFYcbdVe+BJ94Kl/pgjSPYHLzqEdAMXTA1BH9WDx7iJ+9/iDajPF64LtzWX8C1mCfpbMZjJGhAOw==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.36.1", - "comment-parser": "1.3.1", + "@es-joy/jsdoccomment": "~0.42.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", - "esquery": "^1.4.0", - "semver": "^7.3.8", - "spdx-expression-parse": "^3.0.1" + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.6.0", + "spdx-expression-parse": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || ^17 || ^18 || ^19" + "node": ">=18" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { @@ -6074,6 +6097,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -7809,6 +7842,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -8434,9 +8482,9 @@ } }, "node_modules/jsdoc-type-pratt-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz", - "integrity": "sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", "dev": true, "engines": { "node": ">=12.0.0" diff --git a/package.json b/package.json index 2c27300904..e0cc1f3332 100644 --- a/package.json +++ b/package.json @@ -230,7 +230,7 @@ "conventional-commits-parser": "^3.2.4", "eslint": "^8.24.0", "eslint-plugin-disable": "^2.0.1", - "eslint-plugin-jsdoc": "^39.3.6", + "eslint-plugin-jsdoc": "^48.0.5", "eslint-plugin-sonarjs": "^0.18.0", "express": "*", "git-raw-commits": "^2.0.11", diff --git a/test/unit/shim/transaction-shim.test.js b/test/unit/shim/transaction-shim.test.js index 7a49a1f52b..d9bac27da2 100644 --- a/test/unit/shim/transaction-shim.test.js +++ b/test/unit/shim/transaction-shim.test.js @@ -8,6 +8,7 @@ const tap = require('tap') const hashes = require('../../../lib/util/hashes') const helper = require('../../lib/agent_helper') +const { TransactionSpec } = require('../../../lib/shim/specs') const TransactionShim = require('../../../lib/shim/transaction-shim') const notRunningStates = ['stopped', 'stopping', 'errored'] @@ -144,13 +145,16 @@ tap.test('TransactionShim', function (t) { t.afterEach(afterEach) t.test('should not wrap non-functions', function (t) { - shim.bindCreateTransaction(wrappable, 'name', { type: shim.WEB }) + shim.bindCreateTransaction(wrappable, 'name', new TransactionSpec({ type: shim.WEB })) t.notOk(shim.isWrapped(wrappable.name)) t.end() }) t.test('should wrap the first parameter if no properties are given', function (t) { - const wrapped = shim.bindCreateTransaction(wrappable.bar, { type: shim.WEB }) + const wrapped = shim.bindCreateTransaction( + wrappable.bar, + new TransactionSpec({ type: shim.WEB }) + ) t.not(wrapped, wrappable.bar) t.ok(shim.isWrapped(wrapped)) t.equal(shim.unwrap(wrapped), wrappable.bar) @@ -158,7 +162,11 @@ tap.test('TransactionShim', function (t) { }) t.test('should wrap the first parameter if `null` is given for properties', function (t) { - const wrapped = shim.bindCreateTransaction(wrappable.bar, null, { type: shim.WEB }) + const wrapped = shim.bindCreateTransaction( + wrappable.bar, + null, + new TransactionSpec({ type: shim.WEB }) + ) t.not(wrapped, wrappable.bar) t.ok(shim.isWrapped(wrapped)) t.equal(shim.unwrap(wrapped), wrappable.bar) @@ -167,7 +175,7 @@ tap.test('TransactionShim', function (t) { t.test('should replace wrapped properties on the original object', function (t) { const original = wrappable.bar - shim.bindCreateTransaction(wrappable, 'bar', { type: shim.WEB }) + shim.bindCreateTransaction(wrappable, 'bar', new TransactionSpec({ type: shim.WEB })) t.not(wrappable.bar, original) t.ok(shim.isWrapped(wrappable, 'bar')) t.equal(shim.unwrap(wrappable, 'bar'), original) @@ -184,17 +192,14 @@ tap.test('TransactionShim', function (t) { let executed = false const context = {} const value = {} - const wrapped = shim.bindCreateTransaction( - function (a, b, c) { - executed = true - t.equal(this, context) - t.equal(a, 'a') - t.equal(b, 'b') - t.equal(c, 'c') - return value - }, - { type: shim.WEB } - ) + const wrapped = shim.bindCreateTransaction(function (a, b, c) { + executed = true + t.equal(this, context) + t.equal(a, 'a') + t.equal(b, 'b') + t.equal(c, 'c') + return value + }, new TransactionSpec({ type: shim.WEB })) t.notOk(executed) const ret = wrapped.call(context, 'a', 'b', 'c') @@ -204,12 +209,20 @@ tap.test('TransactionShim', function (t) { }) t.test('should create a transaction with the correct type', function (t) { - shim.bindCreateTransaction(wrappable, 'getActiveSegment', { type: shim.WEB }) + shim.bindCreateTransaction( + wrappable, + 'getActiveSegment', + new TransactionSpec({ type: shim.WEB }) + ) const segment = wrappable.getActiveSegment() t.equal(segment.transaction.type, shim.WEB) shim.unwrap(wrappable, 'getActiveSegment') - shim.bindCreateTransaction(wrappable, 'getActiveSegment', { type: shim.BG }) + shim.bindCreateTransaction( + wrappable, + 'getActiveSegment', + new TransactionSpec({ type: shim.BG }) + ) const bgSegment = wrappable.getActiveSegment() t.equal(bgSegment.transaction.type, shim.BG) t.end() @@ -220,21 +233,15 @@ tap.test('TransactionShim', function (t) { let bgTx = null let webCalled = false let bgCalled = false - const bg = shim.bindCreateTransaction( - function () { - bgCalled = true - bgTx = shim.getSegment().transaction - }, - { type: shim.BG } - ) - const web = shim.bindCreateTransaction( - function () { - webCalled = true - webTx = shim.getSegment().transaction - bg() - }, - { type: shim.WEB } - ) + const bg = shim.bindCreateTransaction(function () { + bgCalled = true + bgTx = shim.getSegment().transaction + }, new TransactionSpec({ type: shim.BG })) + const web = shim.bindCreateTransaction(function () { + webCalled = true + webTx = shim.getSegment().transaction + bg() + }, new TransactionSpec({ type: shim.WEB })) web() t.ok(webCalled) @@ -249,13 +256,10 @@ tap.test('TransactionShim', function (t) { let callbackCalled = false let transaction = null - const wrapped = shim.bindCreateTransaction( - () => { - callbackCalled = true - transaction = shim.tracer.getTransaction() - }, - { type: shim.BG } - ) + const wrapped = shim.bindCreateTransaction(() => { + callbackCalled = true + transaction = shim.tracer.getTransaction() + }, new TransactionSpec({ type: shim.BG })) wrapped() @@ -276,25 +280,19 @@ tap.test('TransactionShim', function (t) { t.beforeEach(function () { beforeEach() transactions = [] - web = shim.bindCreateTransaction( - function (cb) { - transactions.push(shim.getSegment().transaction) - if (cb) { - cb() - } - }, - { type: shim.WEB, nest: true } - ) + web = shim.bindCreateTransaction(function (cb) { + transactions.push(shim.getSegment().transaction) + if (cb) { + cb() + } + }, new TransactionSpec({ type: shim.WEB, nest: true })) - bg = shim.bindCreateTransaction( - function (cb) { - transactions.push(shim.getSegment().transaction) - if (cb) { - cb() - } - }, - { type: shim.BG, nest: true } - ) + bg = shim.bindCreateTransaction(function (cb) { + transactions.push(shim.getSegment().transaction) + if (cb) { + cb() + } + }, new TransactionSpec({ type: shim.BG, nest: true })) }) t.afterEach(afterEach) @@ -341,13 +339,10 @@ tap.test('TransactionShim', function (t) { let callbackCalled = false let transaction = null - const wrapped = shim.bindCreateTransaction( - () => { - callbackCalled = true - transaction = shim.tracer.getTransaction() - }, - { type: shim.BG, nest: true } - ) + const wrapped = shim.bindCreateTransaction(() => { + callbackCalled = true + transaction = shim.tracer.getTransaction() + }, new TransactionSpec({ type: shim.BG, nest: true })) wrapped() diff --git a/test/unit/shim/webframework-shim.test.js b/test/unit/shim/webframework-shim.test.js index 7b0cfc5f58..c64789c1c7 100644 --- a/test/unit/shim/webframework-shim.test.js +++ b/test/unit/shim/webframework-shim.test.js @@ -12,6 +12,8 @@ const Shim = require('../../../lib/shim/shim') const WebFrameworkShim = require('../../../lib/shim/webframework-shim') const symbols = require('../../../lib/symbols') +test.runOnly = true + test('WebFrameworkShim', function (t) { t.autoend() let agent = null @@ -603,7 +605,7 @@ test('WebFrameworkShim', function (t) { const filePathSplit = attrs['code.filepath'].split('/') t.equal(filePathSplit[filePathSplit.length - 1], 'webframework-shim.test.js') t.equal(attrs['code.function'], 'getActiveSegment') - t.equal(attrs['code.lineno'], 37) + t.equal(attrs['code.lineno'], 39) t.equal(attrs['code.column'], 50) t.end() })