Repository: newrelic/newrelic-node-apollo-server-plugin (js)

Issue:
Add the ability to Ignore based on other details, like Transaction Name
## Feature Description
Right now, we're only able to ignore based on URL patterns using the [rules->ignore,](https://docs.newrelic.com/docs/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/#rules_ignore) config settings or the `NEW_RELIC_IGNORING_RULES` ENV variable. This isn't super helpful when dealing with GraphQL, since the URL is pretty much always the same. We would like to be able to ignore certain requests based on a pattern applied against the `transaction.name` (or open it up to more attributes, but personally we would like `transaction.name`). The reason for this is that we don't want to see Introspection Requests in our APM.

## Describe Alternatives
Another way is that we could just have a config setting `ignoreIntrospection` that we can set to `true` or `false` and pass into the config when creating the plugin:
```typescript
const createPlugin = require('@newrelic/apollo-server-plugin')
const plugin = createPlugin({
  ignoreIntrospection: true
})

// imported from supported module
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [plugin]
})
```

## Priority
Really Want


Pull request description:
Provided a new config option to control ignoring transactions for
introspection queries

<!--
Thank you for submitting a Pull Request.

This code is leveraged to monitor critical services. Please consider the following:
* Tests are required.
* Performance matters.
* Features that are specific to just your app are unlikely to make it in.

Please fill out the relevant sections as follows:
* Proposed Release Notes: Bulleted list of recommended release notes for the change(s).
* Links: Any relevant links for the change.
* Details: In-depth description of changes, other technical notes, etc.
-->

## Proposed Release Notes
 * Provided a new config option to plugin `captureIntrospectionQueries` to ignore transactions for Introspection Queries

## Links
Closes #78 

## Details
GraphQL provides an ability to make introspection queries: [Introspection](https://graphql.org/learn/introspection/). This is not suggested to be enabled in production, [Why you should disable GraphQL Introspection in Production](https://www.apollographql.com/blog/graphql/security/why-you-should-disable-graphql-introspection-in-production/), but in case you still have it on this config option to our plugin will provide a way to ignore these transactions.  

It's also worth noting that GraphQL has more [introspection types](https://github.com/graphql/graphql-js/blob/main/src/type/introspection.ts#L531) but this solution mirrors Apollo Server's suppression of only `__schema` and `__type`, [NoIntrospection validator](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-core/src/ApolloServer.ts#L94)

**Note**: Based on my tests resolvers never fire to get this information so it's probably safe to never short circuit our instrumentation but instead just flag the transaction as ignored.  


Patch:
diff --git a/README.md b/README.md
index c76cf9d..5ef01b7 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,8 @@ To override configuration, invoke the `createPlugin` function prior to passing t
 // index.js
 const createPlugin = require('@newrelic/apollo-server-plugin')
 const plugin = createPlugin({
-  captureScalars: false
+  captureScalars: false,
+  captureIntrospectionQueries: true
 })
 
 // imported from supported module
@@ -95,7 +96,8 @@ Configuration may be passed into the `createPlugin` function to override specifi
 
 ```js
 const plugin = createPlugin({
-  captureScalars: true
+  captureScalars: true,
+  captureIntrospectionQueries: false
 })
 ```
 
@@ -106,6 +108,8 @@ const plugin = createPlugin({
 
   **NOTE:** query/mutation resolvers will always be captured even if returning a scalar type.
 
+* `[captureIntrospectionQueries = true]` Enable capture of timings for an [IntrospectionQuery](https://graphql.org/graphql-js/utilities/#introspectionquery).
+
 ### Transactions
 
 [Transaction Documentation](./docs/transactions.md)
diff --git a/lib/create-plugin.js b/lib/create-plugin.js
index bc8741f..126dc82 100644
--- a/lib/create-plugin.js
+++ b/lib/create-plugin.js
@@ -29,7 +29,7 @@ const OPERATION_TYPE_ATTR = 'graphql.operation.type'
 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 DESTINATIONS = {
   NONE: 0x00
 }
@@ -60,6 +60,9 @@ function createPlugin(instrumentationApi, config = {}) {
   logger.info('Apollo Server plugin created.')
 
   config.captureScalars = config.captureScalars || false
+  config.captureIntrospectionQueries = config.captureIntrospectionQueries === false
+    ? false
+    : true
 
   logger.debug('Plugin configuration: ', config)
 
@@ -97,6 +100,23 @@ function createPlugin(instrumentationApi, config = {}) {
       }
 
       return {
+        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.')
+
+            const transaction = instrumentationApi.tracer.getTransaction()
+            if (transaction) {
+              transaction.setForceIgnore(true)
+            }
+          }
+        },
         didEncounterErrors(errorsRequestContext) {
           // Since we don't set the operation segment as active, we want to apply the
           // operation segment as active while setting the error to appropriately assign
@@ -411,4 +431,15 @@ function createModuleUsageMetric(agent) {
   ).incrementCallCount()
 }
 
+function isIntrospectionQuery(operation, captureIntrospectionQueries) {
+  if (captureIntrospectionQueries) {
+    return false
+  }
+
+  return operation.selectionSet.selections.every((selection) => {
+    const fieldName = selection.name.value
+    return INTROSPECTION_TYPES.includes(fieldName)
+  })
+}
+
 module.exports = createPlugin


Test patch:
diff --git a/tests/integration/config-capture-introspection-queries.test.js b/tests/integration/config-capture-introspection-queries.test.js
new file mode 100644
index 0000000..80b0567
--- /dev/null
+++ b/tests/integration/config-capture-introspection-queries.test.js
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 New Relic Corporation. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict'
+
+const { executeQuery } = require('../test-client')
+const { setupEnvConfig } = require('../agent-testing')
+const { setupApolloServerTests } = require('../apollo-server-setup')
+const queries = [
+  `{
+    __schema {
+      queryType {
+        fields {
+          name
+        }
+      }
+    }
+  }`,
+  `query introspectionType {
+    __type(name: "Library") {
+      fields {
+        name
+      }
+    }
+  }`
+]
+
+setupApolloServerTests({
+  suiteName: 'default',
+  createTests: createCaptureIntrospectionTests.bind(null, false)
+})
+
+setupApolloServerTests({
+  suiteName: 'captureIntrospectionQueries: true',
+  createTests: createCaptureIntrospectionTests.bind(null, false),
+  pluginConfig: {
+    captureIntrospectionQueries: true
+  }
+})
+
+setupApolloServerTests({
+  suiteName: 'captureIntrospectionQueries: false',
+  createTests: createCaptureIntrospectionTests.bind(null, true),
+  pluginConfig: {
+    captureIntrospectionQueries: false
+  }
+})
+
+
+function createCaptureIntrospectionTests(ignore, t) {
+  setupEnvConfig(t)
+
+  queries.forEach((query) => {
+    t.test(`should ${ignore ? '' : 'not '}ignore transaction when
+captureIntrospectionQuery is ${!ignore} and query contains
+introspection types`, (t) => {
+      const { helper, serverUrl } = t.context
+
+
+      helper.agent.on('transactionFinished', (transaction) => {
+        t.equal(
+          transaction.ignore,
+          ignore,
+          `should set transaction.ignore to ${ignore}`
+        )
+      })
+
+      executeQuery(serverUrl, query, (err) => {
+        t.error(err)
+        t.end()
+      })
+    })
+  })
+
+  t.test(`should not ignore transaction when
+captureIntrospectionQuery is ${!ignore} and query
+does not contain an introspection type`, (t) => {
+      const { helper, serverUrl } = t.context
+
+
+      helper.agent.on('transactionFinished', (transaction) => {
+        t.notOk(
+          transaction.ignore,
+          'should set transaction.ignore to false when not an introspection type'
+        )
+      })
+
+      const query = `query GetAllForLibrary {
+        library(branch: "downtown") {
+          books {
+            title
+          }
+        }
+      }`
+      executeQuery(serverUrl, query, (err) => {
+        t.error(err)
+        t.end()
+      })
+  })
+}
+