@newrelic/apollo-server-plugin
Advanced tools
Comparing version 0.3.0 to 1.0.0
@@ -30,5 +30,9 @@ /* | ||
const OPERATION_NAME_ATTR = 'graphql.operation.name' | ||
const OPERATION_PATH_ATTR = 'graphql.operation.deepestPath' | ||
const OPERATION_QUERY_ATTR = 'graphql.operation.query' | ||
const INTROSPECTION_TYPES = ['__schema', '__type'] | ||
const IGNORED_PATH_FIELDS = ['id', '__typename'] | ||
const SERVICE_DEFINITION_QUERY_NAME = '__ApolloGetServiceDefinition__' | ||
const HEALTH_CHECK_QUERY_NAME = '__ApolloServiceHealthCheck__' | ||
const DESTINATIONS = { | ||
@@ -65,2 +69,4 @@ NONE: 0x00 | ||
config.captureIntrospectionQueries = config.captureIntrospectionQueries || false | ||
config.captureServiceDefinitionQueries = config.captureServiceDefinitionQueries || false | ||
config.captureHealthCheckQueries = config.captureHealthCheckQueries || false | ||
@@ -99,12 +105,3 @@ logger.debug('Plugin configuration: ', config) | ||
didResolveOperation(resolveContext) { | ||
if ( | ||
isIntrospectionQuery( | ||
resolveContext.operation, | ||
config.captureIntrospectionQueries | ||
) | ||
) { | ||
logger.trace('Request is an introspection query and ' + | ||
'`config.captureIntrospectionQueries` is set to `false`. ' + | ||
'Force ignoring the transaction.') | ||
if (shouldIgnoreTransaction(resolveContext.operation, config, logger)) { | ||
const transaction = instrumentationApi.tracer.getTransaction() | ||
@@ -220,3 +217,3 @@ if (transaction) { | ||
if (operationDetails) { | ||
const {operationName, operationType, deepestPath} = operationDetails | ||
const {operationName, operationType, deepestUniquePath} = operationDetails | ||
@@ -233,6 +230,4 @@ operationSegment.addAttribute(OPERATION_TYPE_ATTR, operationType) | ||
// Certain requests, such as introspection, won't hit any resolvers | ||
if (deepestPath) { | ||
operationSegment.addAttribute(OPERATION_PATH_ATTR, deepestPath) | ||
formattedOperation += `/${deepestPath}` | ||
if (deepestUniquePath) { | ||
formattedOperation += `/${deepestUniquePath}` | ||
} | ||
@@ -297,3 +292,4 @@ | ||
const deepestSelectionPath = getDeepestSelectionPath(definition) | ||
const deepestUniquePath = getDeepestUniqueSelection(definition) | ||
const definitionName = definition.name && definition.name.value | ||
@@ -304,3 +300,3 @@ | ||
operationName: definitionName, | ||
deepestPath: deepestSelectionPath.join('.') | ||
deepestUniquePath: deepestUniquePath.join('.') | ||
} | ||
@@ -350,35 +346,56 @@ } | ||
/** | ||
* Returns the deepest path as an array of parts | ||
* from an apollo-server document definition. | ||
* Checks if selection is an InlineFragment that is a | ||
* NamedType | ||
* see: https://graphql.org/learn/queries/#inline-fragments | ||
* | ||
* @param {Object} selection node in grapql document AST | ||
*/ | ||
function getDeepestSelectionPath(definition) { | ||
let deepestPath = null | ||
function isNamedType(selection) { | ||
return selection.kind === 'InlineFragment' && | ||
selection.typeCondition && | ||
selection.typeCondition.kind === 'NamedType' && | ||
selection.typeCondition.name | ||
} | ||
definition.selectionSet.selections.forEach((selection) => { | ||
searchSelection(selection) | ||
}) | ||
/** | ||
* Returns the deepest path in the document definition selectionSet | ||
* where only one field was selected. | ||
* | ||
* 'id' and '__typename' fields are filtered out of consideration to improve | ||
* naming in sub graph scenarioes. | ||
*/ | ||
function getDeepestUniqueSelection(definition) { | ||
const deepestPath = [] | ||
let selection = definition | ||
while (selection.selectionSet) { | ||
const filtered = selection.selectionSet.selections.filter((currentSelection) => { | ||
// Inline fragments describe the prior element (_entities or unions) but contain | ||
// selections for further naming. | ||
if (currentSelection.kind === 'InlineFragment') { | ||
return true | ||
} | ||
return deepestPath | ||
return IGNORED_PATH_FIELDS.indexOf(currentSelection.name.value) < 0 | ||
}) | ||
/** | ||
* Search each selection path until no-more sub-selections | ||
* exist. If the curent path is deeper than deepestPath, | ||
* deepestPath is replaced. | ||
*/ | ||
function searchSelection(selection, currentParts) { | ||
const parts = currentParts ? [...currentParts] : [] | ||
if (filtered.length === 0 || filtered.length > 1) { | ||
// selections not unique OR | ||
// only one IGNORED_PATH_FIELDS item in selections | ||
break | ||
} | ||
// we have found that when queries contain InlineFragments | ||
// they lack `name` property, check for property before adding to parts | ||
// see https://github.com/newrelic/newrelic-node-apollo-server-plugin/issues/84 | ||
selection.name && parts.push(selection.name.value) | ||
selection = filtered[0] | ||
if (selection.selectionSet) { | ||
selection.selectionSet.selections.forEach((innerSelection) => { | ||
searchSelection(innerSelection, parts) | ||
}) | ||
} else if (!deepestPath || parts.length > deepestPath.length) { | ||
deepestPath = parts | ||
if (isNamedType(selection)) { | ||
const lastItemIdx = deepestPath.length - 1 | ||
// add type to the last item in deepestPath array | ||
// (i.e - `_entities<Human>`) | ||
deepestPath[lastItemIdx] = | ||
`${deepestPath[lastItemIdx]}<${selection.typeCondition.name.value}>` | ||
} else { | ||
selection.name && deepestPath.push(selection.name.value) | ||
} | ||
} | ||
return deepestPath | ||
} | ||
@@ -427,7 +444,3 @@ | ||
function isIntrospectionQuery(operation, captureIntrospectionQueries) { | ||
if (captureIntrospectionQueries) { | ||
return false | ||
} | ||
function isIntrospectionQuery(operation) { | ||
return operation.selectionSet.selections.every((selection) => { | ||
@@ -439,2 +452,47 @@ const fieldName = selection.name.value | ||
function isServiceDefinitionQuery(operation) { | ||
return operation.name && operation.name.value === SERVICE_DEFINITION_QUERY_NAME | ||
} | ||
function isHealthCheckQuery(operation) { | ||
return operation.name && operation.name.value === HEALTH_CHECK_QUERY_NAME | ||
} | ||
function shouldIgnoreTransaction(operation, config, logger) { | ||
if ( | ||
!config.captureIntrospectionQueries && | ||
isIntrospectionQuery(operation) | ||
) { | ||
logger.trace('Request is an introspection query and ' + | ||
'`config.captureIntrospectionQueries` is set to `false`. ' + | ||
'Force ignoring the transaction.') | ||
return true | ||
} | ||
if ( | ||
!config.captureServiceDefinitionQueries && | ||
isServiceDefinitionQuery(operation) | ||
) { | ||
logger.trace('Request is an Apollo Federated Gateway service definition query and ' + | ||
'`config.captureServiceDefinitionQueries` is set to `false`. ' + | ||
'Force ignoring the transaction.') | ||
return true | ||
} | ||
if ( | ||
!config.captureHealthCheckQueries && | ||
isHealthCheckQuery(operation) | ||
) { | ||
logger.trace('Request is an Apollo Federated Gateway health check query and ' + | ||
'`config.captureHealthCheckQueries` is set to `false`. ' + | ||
'Force ignoring the transaction.') | ||
return true | ||
} | ||
return false | ||
} | ||
module.exports = createPlugin |
{ | ||
"name": "@newrelic/apollo-server-plugin", | ||
"version": "0.3.0", | ||
"version": "1.0.0", | ||
"description": "Apollo Server plugin that adds New Relic Node.js agent instrumentation.", | ||
@@ -14,3 +14,3 @@ "main": "./index.js", | ||
"versioned:folder": "versioned-tests --minor -i 2", | ||
"versioned:npm6": "versioned-tests --minor -i 2 'tests/versioned/*'", | ||
"versioned:npm6": "versioned-tests --minor -i 2 'tests/versioned/**/!(apollo-server-koa)' && versioned-tests --minor --all -i 2 'tests/versioned/apollo-server-koa'", | ||
"versioned:npm7": "versioned-tests --minor --all -i 2 'tests/versioned/*'" | ||
@@ -33,3 +33,3 @@ }, | ||
"engines": { | ||
"node": ">=10.0.0" | ||
"node": ">=12.0.0" | ||
}, | ||
@@ -36,0 +36,0 @@ "devDependencies": { |
@@ -46,20 +46,2 @@ [![Community Plus header](https://github.com/newrelic/opensource-website/raw/master/src/images/categories/Community_Plus.png)](https://opensource.newrelic.com/oss-category/#community-plus) | ||
To override configuration, invoke the `createPlugin` function prior to passing to Apollo Server: | ||
```js | ||
// index.js | ||
const createPlugin = require('@newrelic/apollo-server-plugin') | ||
const plugin = createPlugin({ | ||
captureScalars: false, | ||
captureIntrospectionQueries: false | ||
}) | ||
// imported from supported module | ||
const server = new ApolloServer({ | ||
typeDefs, | ||
resolvers, | ||
plugins: [plugin] | ||
}) | ||
``` | ||
## Usage | ||
@@ -96,3 +78,3 @@ | ||
Configuration may be passed into the `createPlugin` function to override specific values. The configuration object and all properties are optional. | ||
Configuration may be passed into the `createPlugin` function to override specific values. To override configuration, invoke the `createPlugin` function prior to passing to Apollo Server. The configuration object and all properties are optional. | ||
@@ -102,3 +84,5 @@ ```js | ||
captureScalars: true, | ||
captureIntrospectionQueries: true | ||
captureIntrospectionQueries: true, | ||
captureServiceDefinitionQueries: true, | ||
captureHealthCheckQueries: true | ||
}) | ||
@@ -116,2 +100,6 @@ ``` | ||
* `[captureServiceDefinitionQueries = false]` Enable capture of timings for a [Service Definition query](https://www.apollographql.com/docs/federation/federation-spec/#fetch-service-capabilities) received from an Apollo Federated Gateway Server. | ||
* `[captureHealthCheckQueries = false]` Enable capture of timings for a [Health Check query](https://www.apollographql.com/docs/federation/api/apollo-gateway/#servicehealthcheck) received from an Apollo Federated Gateway Server. | ||
### Apollo Federation Support | ||
@@ -178,3 +166,3 @@ | ||
`post /query/<anonymous>/libraries.books.author.name` | ||
`post /query/<anonymous>/libraries.books` | ||
@@ -191,5 +179,5 @@ For more information on how transactions are named, including how query errors may impact naming, please see the [transaction documentation](./docs/transactions.md). | ||
`/GraphQL/operation/ApolloServer/[operation-type]/[operation-name]/[deepest-path]` | ||
`/GraphQL/operation/ApolloServer/[operation-type]/[operation-name]/[deepest-unique-path]` | ||
Operation metrics are very similar to how transaction names are constructed including the operation type, operation name and deepest-path. These metrics represent the durations of the individual queries or mutations and can be used to compare outside of the context of individual transactions which may have multiple queries. | ||
Operation metrics are very similar to how transaction names are constructed including the operation type, operation name and deepest unique path. These metrics represent the durations of the individual queries or mutations and can be used to compare outside of the context of individual transactions which may have multiple queries. | ||
@@ -225,7 +213,7 @@ If you would like to have a list of the top 10 slowest operations, the following query can be used to pull the data on demand or as a part of a dashboard. The 'Bar' chart type is a recommended visualization for this query. | ||
`/GraphQL/operation/ApolloServer/[operation-type]/[operation-name]/[deepest-path]` | ||
`/GraphQL/operation/ApolloServer/[operation-type]/[operation-name]/[deepest-unique-path]` | ||
Operation segments/spans include the operation type, operation name and deepest-path. These represent the individual duration and attributes of a specific invocation within a transaction or trace. | ||
Operation segments/spans include the operation type, operation name and deepest unique path. These represent the individual duration and attributes of a specific invocation within a transaction or trace. | ||
The operation type, operation name and deepest-path are captured as attributes on a segment or span as well as the query with obfuscated arguments. | ||
The operation type and operation name are captured as attributes on a segment or span as well as the query with obfuscated arguments. | ||
@@ -232,0 +220,0 @@ For more information on collected attributes, see the [segments and spans documentation](./docs/segments-and-spans.md) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
44702
452
0
295