Repository: newrelic/newrelic-node-apollo-server-plugin
Pull request #46 (2020-10-12 22:47:57)

Exclude scalars
<!--
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

## Links

## Details

Nested scalar fields (included defined as non-null) will no-longer be captured as spans/metrics by default. This means that time (if any) will be associated with the operation itself. Top level scalar fields (query resolver that returns a string for example) will still get captured.

Captured:

```
type Query {
    books: [Book] // Captured
    hello: String // Captured
}

type Book {
    title: String! // NOT CAPTURED
    author: Author! // Captured
}
```

The plugin nowr eturns a `createPlugin` function that allows passing of configuration. This is pre-bound to the instrumentation API.

Currently, the configuration will allow for re-enabling of scalar fields should that be desired via `{ captureScalars: true }`. **NOTE: we could also ship without this option and wait to see if needed?**.

Since Apollo Server will invoke any plugin passed in as a function, customers can still use the previous approach.

```js
const plugin = require(@newrelic/apollo-server-plugin)
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [plugin]
});
```

OR

```js
const createPlugin = require(@newrelic/apollo-server-plugin)
const plugin = createPlugin({captureScalars: true})
const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [plugin]
});
```

This does not solve the issue of many nested resolvers used on list items. 



Patch:
diff --git a/index.js b/index.js
index df009f2..cb6eaff 100644
--- a/index.js
+++ b/index.js
@@ -10,4 +10,4 @@ const createPlugin = require('./lib/create-plugin')
 
 // TODO: need to grab instrumentation API from agent via
 // supported means that will not dissapear when agent disabled.
-module.exports = createPlugin(newrelic.shim)
+module.exports = createPlugin.bind(null, newrelic.shim)
diff --git a/lib/create-plugin.js b/lib/create-plugin.js
index 90c1303..8a297e6 100644
--- a/lib/create-plugin.js
+++ b/lib/create-plugin.js
@@ -34,15 +34,41 @@ const DESTINATIONS = {
   NONE: 0x00
 }
 
-function createPlugin(instrumentationApi) {
+/**
+ * @typedef {object} PluginConfig
+ * @property {boolean} [captureScalars=false]
+ * Enable capture of timing of fields resolved with the `GraphQLScalarType` return type.
+ * This may be desired when performing time intensive calculations to return a scalar
+ * value.This is not recommended for queries that return a large number of pre-calculated
+ * scalar fields.
+ * NOTE: query/mutation resolvers will always be captured even if returning a scalar type.
+ */
+
+/**
+ * Creates an Apollo Server plugin for capturing timing data
+ * via the New Relic Node.js agent.
+ * @param {*} instrumentationApi New Relic instrumentation API
+ * @param {PluginConfig} [config]
+ */
+function createPlugin(instrumentationApi, config = {}) {
   if (!instrumentationApi) {
     return {}
   }
 
+  const logger = instrumentationApi.logger.child({component: 'ApolloServerPlugin'})
+
+  logger.info('Apollo Server plugin created.')
+
+  config.captureScalars = config.captureScalars || false
+
+  logger.debug('Plugin configuration: ', config)
+
   createModuleUsageMetric(instrumentationApi.agent)
 
   return {
     requestDidStart(requestContext) {
+      logger.trace('Begin requestDidStart')
+
       const requestParent = instrumentationApi.getActiveSegment()
 
       // We do not set to active here as batched queries will hit this
@@ -74,6 +100,18 @@ function createPlugin(instrumentationApi) {
         executionDidStart() {
           return {
             willResolveField({args, info}) {
+              const pathArray = flattenToArray(info.path)
+              const formattedPath = pathArray.reverse().join('.')
+
+              if (pathArray.length > deepestResolvedPath.depth) {
+                deepestResolvedPath.depth = pathArray.length
+                deepestResolvedPath.formatted = formattedPath
+              }
+
+              if (!config.captureScalars && !isTopLevelField(info) && isScalar(info)) {
+                return null
+              }
+
               const currentSeg = instrumentationApi.getActiveSegment()
 
               // Nest everything under operation as resolvers start/finish
@@ -88,15 +126,8 @@ function createPlugin(instrumentationApi) {
               resolverSegment.start()
               instrumentationApi.setActiveSegment(resolverSegment)
 
-              const pathArray = flattenToArray(info.path)
-              const formattedPath = pathArray.reverse().join('.')
-
               resolverSegment.name = `${RESOLVE_PREFIX}/${formattedPath}`
 
-              if (pathArray.length > deepestResolvedPath.depth) {
-                deepestResolvedPath.depth = pathArray.length
-                deepestResolvedPath.formatted = formattedPath
-              }
               resolverSegment.addAttribute(FIELD_PATH_ATTR, formattedPath)
               resolverSegment.addAttribute(FIELD_NAME_ATTR, info.fieldName)
               resolverSegment.addAttribute(RETURN_TYPE_ATTR, info.returnType.toString())
@@ -172,6 +203,8 @@ function createPlugin(instrumentationApi) {
 
           operationSegment.name = `${OPERATION_PREFIX}/${segmentName}`
           operationSegment.end()
+
+          logger.trace('End willSendResponse')
         }
       }
     }
@@ -196,6 +229,36 @@ function getOperationDetails(responseContext, deepestPath) {
   return null
 }
 
+function isScalar(fieldInfo) {
+  const result =
+    isScalarType(fieldInfo.returnType) || isNonNullScalarType(fieldInfo.returnType)
+  return result
+}
+
+function isScalarType(typeInstance) {
+  const typeName = typeInstance.constructor.name
+  const result = typeName === 'GraphQLScalarType'
+  return result
+}
+
+function isNonNullScalarType(returnType) {
+  const returnTypeName = returnType.constructor.name
+  if (returnTypeName !== 'GraphQLNonNull' || !returnType.ofType) {
+    return false
+  }
+
+  const nestedType = returnType.ofType
+  const result = isScalarType(nestedType)
+
+  return result
+}
+
+function isTopLevelField(fieldInfo) {
+  const parentName = fieldInfo.parentType.name
+  const result = parentName === 'Query' || parentName === 'Mutation'
+  return result
+}
+
 function getDetailsFromDocument(document) {
   const definition = document.definitions[0]
 


Test patch:
diff --git a/tests/apollo-server-setup.js b/tests/apollo-server-setup.js
index 9cb8537..5fc0648 100644
--- a/tests/apollo-server-setup.js
+++ b/tests/apollo-server-setup.js
@@ -14,7 +14,7 @@ const { getTypeDefs, resolvers } = require('./data-definitions')
 
 const WEB_FRAMEWORK = 'Expressjs'
 
-function setupApolloServerTests({suiteName, createTests}, config) {
+function setupApolloServerTests({suiteName, createTests, pluginConfig}, agentConfig) {
   tap.test(`apollo-server: ${suiteName}`, (t) => {
     t.autoend()
 
@@ -24,11 +24,11 @@ function setupApolloServerTests({suiteName, createTests}, config) {
 
     t.beforeEach((done) => {
       // load default instrumentation. express being critical
-      helper = utils.TestAgent.makeInstrumented(config)
+      helper = utils.TestAgent.makeInstrumented(agentConfig)
       const createPlugin = require('../lib/create-plugin')
       const nrApi = helper.getAgentApi()
 
-      const plugin = createPlugin(nrApi.shim)
+      const plugin = createPlugin(nrApi.shim, pluginConfig)
 
       // Do after instrumentation to ensure express isn't loaded too soon.
       const { ApolloServer, gql } = require('apollo-server')
@@ -38,7 +38,7 @@ function setupApolloServerTests({suiteName, createTests}, config) {
         plugins: [plugin]
       })
 
-      server.listen().then(({ url }) => {
+      server.listen({port: 0}).then(({ url }) => {
         serverUrl = url
 
         t.context.helper = helper
diff --git a/tests/data-definitions.js b/tests/data-definitions.js
index bf7704f..eb9a8b6 100644
--- a/tests/data-definitions.js
+++ b/tests/data-definitions.js
@@ -26,6 +26,31 @@ const books = [
     isbn: 'a-second-fake-isbn',
     author: 'Faux Hawk',
     branch: 'downtown'
+  },
+  {
+    title: '[Redacted]',
+    isbn: 'a-third-fake-isbn',
+    author: 'Closed Telemetry',
+    branch: 'riverside'
+  },
+  {
+    title: 'Be a hero: fixing the things you broke',
+    isbn: 'a-fourth-fake-isbn',
+    author: '10x Developer',
+    branch: 'downtown'
+  }
+]
+
+const magazines = [
+  {
+    title: 'Reli Updates Weekly',
+    issue: 1,
+    branch: 'riverside'
+  },
+  {
+    title: 'Reli Updates Weekly',
+    issue: 2,
+    branch: 'downtown'
   }
 ]
 
@@ -34,7 +59,7 @@ function getTypeDefs(gql) {
     type Library {
       branch: String!
       books: [Book!]
-      boom: String
+      magazines: [Magazine]
     }
 
     type Book {
@@ -47,13 +72,18 @@ function getTypeDefs(gql) {
       name: String!
     }
 
+    type Magazine {
+      title: String!
+      issue: Int
+    }
+
     type Query {
       books: [Book]
       hello: String
       boom: String
       paramQuery(blah: String!, blee: String): String!
       libraries: [Library]
-      library(branch: String!): [Library]
+      library(branch: String!): Library
     }
 
     type Mutation {
@@ -80,7 +110,7 @@ const resolvers = {
     library: (_, {branch}) => {
       const promise = new Promise((resolve) => {
         setTimeout(() => {
-          const filtered = libraries.filter(library => library.branch === branch)
+          const filtered = libraries.find(library => library.branch === branch)
           resolve(filtered)
         }, 0)
       })
@@ -102,6 +132,9 @@ const resolvers = {
   Library: {
     books(parent) {
       return books.filter(book => book.branch === parent.branch)
+    },
+    magazines(parent) {
+      return magazines.filter(magazine => magazine.branch === parent.branch)
     }
   },
   Book: {
diff --git a/tests/integration/bootstrap.test.js b/tests/integration/bootstrap.test.js
index 2942c51..67478b3 100644
--- a/tests/integration/bootstrap.test.js
+++ b/tests/integration/bootstrap.test.js
@@ -11,20 +11,22 @@ const { setupEnvConfig } = require('../agent-testing')
 
 const INDEX_PATH = '../..'
 
-tap.test('Should export plugin when loaded', (t) => {
-  const plugin = require(INDEX_PATH)
-  t.ok(plugin)
+tap.test('Should export createPlugin when loaded', (t) => {
+  const createPlugin = require(INDEX_PATH)
+  t.ok(createPlugin)
 
   resetModuleCache(() => {
     t.end()
   })
 })
 
-tap.test('should export noop plugin when agent disabled', (t) => {
+tap.test('should create noop plugin when agent disabled', (t) => {
   // w/o a config file the agent will be disabled by default
-  const plugin = require('../..')
-  t.ok(plugin)
+  const createPlugin = require('../..')
+  t.ok(createPlugin)
 
+  const plugin = createPlugin()
+  t.ok(plugin)
   t.notOk(plugin.requestDidStart)
 
   resetModuleCache(() => {
@@ -32,10 +34,13 @@ tap.test('should export noop plugin when agent disabled', (t) => {
   })
 })
 
-tap.test('should export full plugin when agent enabled', (t) => {
+tap.test('should create full plugin when agent enabled', (t) => {
   setupEnvConfig(t)
 
-  const plugin = require('../..')
+  const createPlugin = require('../..')
+  t.ok(createPlugin)
+
+  const plugin = createPlugin()
   t.ok(plugin)
   t.ok(plugin.requestDidStart)
 
diff --git a/tests/integration/config-capture-scalars.test.js b/tests/integration/config-capture-scalars.test.js
new file mode 100644
index 0000000..855cc93
--- /dev/null
+++ b/tests/integration/config-capture-scalars.test.js
@@ -0,0 +1,193 @@
+/*
+ * 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 OPERATION_PREFIX = 'GraphQL/operation/ApolloServer'
+const RESOLVE_PREFIX = 'GraphQL/resolve/ApolloServer'
+const TRANSACTION_PREFIX = 'WebTransaction/Expressjs/POST'
+
+const { setupApolloServerTests } = require('../apollo-server-setup')
+
+setupApolloServerTests({
+  suiteName: 'default',
+  createTests: createNoScalarTests
+})
+
+setupApolloServerTests({
+  suiteName: 'captureScalars: false',
+  createTests: createNoScalarTests,
+  pluginConfig: {
+    captureScalars: false
+  }
+})
+
+setupApolloServerTests({
+  suiteName: 'captureScalars: true',
+  createTests: createScalarTests,
+  pluginConfig: {
+    captureScalars: true
+  }
+})
+
+function createNoScalarTests(t) {
+  setupEnvConfig(t)
+
+  t.test('multi-level, should not capture scalar fields', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'GetAllForLibrary'
+    const query = `query ${expectedName} {
+      library(branch: "downtown") {
+        books {
+          title
+          author {
+            name
+          }
+        }
+        magazines {
+          title
+          issue
+        }
+      }
+    }`
+
+    const deepestPath = 'library.books.author.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [
+          { name: 'Nodejs/Middleware/Expressjs/query' },
+          { name: 'Nodejs/Middleware/Expressjs/expressInit' },
+          {
+            name: 'Expressjs/Router: /',
+            children: [
+              { name: 'Nodejs/Middleware/Expressjs/<anonymous>' },
+              { name: 'Nodejs/Middleware/Expressjs/corsMiddleware' },
+              { name: 'Nodejs/Middleware/Expressjs/jsonParser' },
+              { name: 'Nodejs/Middleware/Expressjs/<anonymous>' },
+              {
+                name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+                children: [{
+                  name: `${OPERATION_PREFIX}/${operationPart}`,
+                  children: [
+                    {
+                      name: `${RESOLVE_PREFIX}/library`,
+                      children: [{
+                        name: 'timers.setTimeout',
+                        children: [{
+                          name: 'Callback: <anonymous>'
+                        }]
+                      }]
+                    },
+                    { name: `${RESOLVE_PREFIX}/library.books` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author` },
+                    { name: `${RESOLVE_PREFIX}/library.magazines`}
+                  ]
+                }
+              ]
+            }
+          ]
+        }]
+      }]
+
+      // Exact match to ensure no extra fields snuck in
+      t.exactSegments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err) => {
+      t.error(err)
+      t.end()
+    })
+  })
+}
+
+function createScalarTests(t) {
+  setupEnvConfig(t)
+
+  t.test('multi-level, should capture scalar fields', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'GetAllForLibrary'
+    const query = `query ${expectedName} {
+      library(branch: "downtown") {
+        books {
+          title
+          author {
+            name
+          }
+        }
+        magazines {
+          title
+          issue
+        }
+      }
+    }`
+
+    const deepestPath = 'library.books.author.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [
+          { name: 'Nodejs/Middleware/Expressjs/query' },
+          { name: 'Nodejs/Middleware/Expressjs/expressInit' },
+          {
+            name: 'Expressjs/Router: /',
+            children: [
+              { name: 'Nodejs/Middleware/Expressjs/<anonymous>' },
+              { name: 'Nodejs/Middleware/Expressjs/corsMiddleware' },
+              { name: 'Nodejs/Middleware/Expressjs/jsonParser' },
+              { name: 'Nodejs/Middleware/Expressjs/<anonymous>' },
+              {
+                name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+                children: [{
+                  name: `${OPERATION_PREFIX}/${operationPart}`,
+                  children: [
+                    {
+                      name: `${RESOLVE_PREFIX}/library`,
+                      children: [{
+                        name: 'timers.setTimeout',
+                        children: [{
+                          name: 'Callback: <anonymous>'
+                        }]
+                      }]
+                    },
+                    { name: `${RESOLVE_PREFIX}/library.books` },
+                    { name: `${RESOLVE_PREFIX}/library.books.title` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author.name` },
+                    { name: `${RESOLVE_PREFIX}/library.books.title` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author` },
+                    { name: `${RESOLVE_PREFIX}/library.books.author.name` },
+                    { name: `${RESOLVE_PREFIX}/library.magazines`},
+                    { name: `${RESOLVE_PREFIX}/library.magazines.title`},
+                    { name: `${RESOLVE_PREFIX}/library.magazines.issue`}
+                  ]
+                }
+              ]
+            }
+          ]
+        }]
+      }]
+
+      // Exact match to ensure no extra fields snuck in
+      t.exactSegments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err) => {
+      t.error(err)
+      t.end()
+    })
+  })
+}
+
diff --git a/tests/integration/metrics.test.js b/tests/integration/metrics.test.js
index 99f282a..409b7c1 100644
--- a/tests/integration/metrics.test.js
+++ b/tests/integration/metrics.test.js
@@ -91,8 +91,7 @@ function createMetricsTests(t) {
         `${OPERATION_PREFIX}/${operationPart}`,
         `${RESOLVE_PREFIX}/libraries`,
         `${RESOLVE_PREFIX}/books`,
-        `${RESOLVE_PREFIX}/author`,
-        `${RESOLVE_PREFIX}/name`
+        `${RESOLVE_PREFIX}/author`
       ])
     })
 
@@ -133,9 +132,7 @@ function createMetricsTests(t) {
         `${OPERATION_PREFIX}/${operationPart1}`,
         `${RESOLVE_PREFIX}/library`,
         `${RESOLVE_PREFIX}/books`,
-        `${RESOLVE_PREFIX}/title`,
-        `${RESOLVE_PREFIX}/author`,
-        `${RESOLVE_PREFIX}/name`
+        `${RESOLVE_PREFIX}/author`
       ]
 
       const operationMetrics2 = [
diff --git a/tests/versioned/apollo-server-express/apollo-server-express-setup.js b/tests/versioned/apollo-server-express/apollo-server-express-setup.js
index 4291c71..85ad153 100644
--- a/tests/versioned/apollo-server-express/apollo-server-express-setup.js
+++ b/tests/versioned/apollo-server-express/apollo-server-express-setup.js
@@ -14,7 +14,7 @@ const { getTypeDefs, resolvers } = require('../../data-definitions')
 
 const WEB_FRAMEWORK = 'Expressjs'
 
-function setupApolloServerExpressTests({suiteName, createTests}, config) {
+function setupApolloServerExpressTests({suiteName, createTests, pluginConfig}, config) {
   tap.test(`apollo-server-express: ${suiteName}`, (t) => {
     t.autoend()
 
@@ -30,7 +30,7 @@ function setupApolloServerExpressTests({suiteName, createTests}, config) {
       const nrApi = helper.getAgentApi()
 
       // TODO: eventually use proper function for instrumenting and not .shim
-      const plugin = createPlugin(nrApi.shim)
+      const plugin = createPlugin(nrApi.shim, pluginConfig)
 
       const express = require('express')
 
diff --git a/tests/versioned/apollo-server-express/segments.test.js b/tests/versioned/apollo-server-express/segments.test.js
index 8bc7a3f..a6f6847 100644
--- a/tests/versioned/apollo-server-express/segments.test.js
+++ b/tests/versioned/apollo-server-express/segments.test.js
@@ -6,6 +6,8 @@
 'use strict'
 
 const { setupApolloServerExpressTests } = require('./apollo-server-express-setup')
-const segmentsTests = require('../express-segments-tests')
+const segmentsDefaultTests = require('../express-segments-default-tests')
+const segmentsScalarTests = require('../express-segments-scalar-tests')
 
-setupApolloServerExpressTests(segmentsTests)
+setupApolloServerExpressTests(segmentsDefaultTests)
+setupApolloServerExpressTests(segmentsScalarTests)
diff --git a/tests/versioned/apollo-server-fastify/apollo-server-fastify-setup.js b/tests/versioned/apollo-server-fastify/apollo-server-fastify-setup.js
index 7a0b66b..5565df0 100644
--- a/tests/versioned/apollo-server-fastify/apollo-server-fastify-setup.js
+++ b/tests/versioned/apollo-server-fastify/apollo-server-fastify-setup.js
@@ -14,7 +14,7 @@ const { getTypeDefs, resolvers } = require('../../data-definitions')
 
 const WEB_FRAMEWORK = 'Nodejs'
 
-function setupApolloServerFastifyTests({suiteName, createTests}, config) {
+function setupApolloServerFastifyTests({suiteName, createTests, pluginConfig}, config) {
   tap.test(`apollo-server-fastify: ${suiteName}`, (t) => {
     t.autoend()
 
@@ -32,7 +32,7 @@ function setupApolloServerFastifyTests({suiteName, createTests}, config) {
       app = require('fastify')()
 
       // TODO: eventually use proper function for instrumenting and not .shim
-      const plugin = createPlugin(nrApi.shim)
+      const plugin = createPlugin(nrApi.shim, pluginConfig)
 
       // Do after instrumentation to ensure hapi isn't loaded too soon.
       const { ApolloServer, gql } = require('apollo-server-fastify')
diff --git a/tests/versioned/apollo-server-fastify/segments.test.js b/tests/versioned/apollo-server-fastify/segments.test.js
index da1d9f5..3cc4877 100644
--- a/tests/versioned/apollo-server-fastify/segments.test.js
+++ b/tests/versioned/apollo-server-fastify/segments.test.js
@@ -17,7 +17,10 @@ const { setupApolloServerFastifyTests } = require('./apollo-server-fastify-setup
 
 setupApolloServerFastifyTests({
   suiteName: 'fastify segments',
-  createTests: createFastifySegmentsTests
+  createTests: createFastifySegmentsTests,
+  pluginConfig: {
+    captureScalars: true
+  }
 })
 
 function createFastifySegmentsTests(t, frameworkName) {
diff --git a/tests/versioned/apollo-server-hapi/apollo-server-hapi-setup.js b/tests/versioned/apollo-server-hapi/apollo-server-hapi-setup.js
index e67815a..5c0ace6 100644
--- a/tests/versioned/apollo-server-hapi/apollo-server-hapi-setup.js
+++ b/tests/versioned/apollo-server-hapi/apollo-server-hapi-setup.js
@@ -14,7 +14,7 @@ const { getTypeDefs, resolvers } = require('../../data-definitions')
 
 const WEB_FRAMEWORK = 'Hapi'
 
-function setupApolloServerHapiTests({suiteName, createTests}, config) {
+function setupApolloServerHapiTests({suiteName, createTests, pluginConfig}, config) {
   tap.test(`apollo-server-hapi: ${suiteName}`, (t) => {
     t.autoend()
 
@@ -30,7 +30,7 @@ function setupApolloServerHapiTests({suiteName, createTests}, config) {
       const nrApi = helper.getAgentApi()
 
       // TODO: eventually use proper function for instrumenting and not .shim
-      const plugin = createPlugin(nrApi.shim)
+      const plugin = createPlugin(nrApi.shim, pluginConfig)
 
       const Hapi = require('@hapi/hapi')
 
diff --git a/tests/versioned/apollo-server-hapi/segments.test.js b/tests/versioned/apollo-server-hapi/segments.test.js
index b048de3..e6064b3 100644
--- a/tests/versioned/apollo-server-hapi/segments.test.js
+++ b/tests/versioned/apollo-server-hapi/segments.test.js
@@ -17,7 +17,10 @@ const { setupApolloServerHapiTests } = require('./apollo-server-hapi-setup')
 
 setupApolloServerHapiTests({
   suiteName: 'hapi segments',
-  createTests: createHapiSegmentsTests
+  createTests: createHapiSegmentsTests,
+  pluginConfig: {
+    captureScalars: true
+  }
 })
 
 function createHapiSegmentsTests(t, frameworkName) {
diff --git a/tests/versioned/apollo-server-koa/apollo-server-koa-setup.js b/tests/versioned/apollo-server-koa/apollo-server-koa-setup.js
index 3e98440..2e5b9c8 100644
--- a/tests/versioned/apollo-server-koa/apollo-server-koa-setup.js
+++ b/tests/versioned/apollo-server-koa/apollo-server-koa-setup.js
@@ -14,7 +14,7 @@ const { getTypeDefs, resolvers } = require('../../data-definitions')
 
 const WEB_FRAMEWORK = 'WebFrameworkUri/Koa'
 
-function setupApolloServerKoaTests({suiteName, createTests}, config) {
+function setupApolloServerKoaTests({suiteName, createTests, pluginConfig}, config) {
   tap.test(`apollo-server-koa: ${suiteName}`, (t) => {
     t.autoend()
 
@@ -35,7 +35,7 @@ function setupApolloServerKoaTests({suiteName, createTests}, config) {
       app = new Koa()
 
       // TODO: eventually use proper function for instrumenting and not .shim
-      const plugin = createPlugin(nrApi.shim)
+      const plugin = createPlugin(nrApi.shim, pluginConfig)
 
       const graphqlPath = '/gql'
 
diff --git a/tests/versioned/apollo-server-koa/segments.test.js b/tests/versioned/apollo-server-koa/segments.test.js
index 2c5b0ff..04a2524 100644
--- a/tests/versioned/apollo-server-koa/segments.test.js
+++ b/tests/versioned/apollo-server-koa/segments.test.js
@@ -17,7 +17,10 @@ const { setupApolloServerKoaTests } = require('./apollo-server-koa-setup')
 
 setupApolloServerKoaTests({
   suiteName: 'koa segments',
-  createTests: createKoaSegmentsTests
+  createTests: createKoaSegmentsTests,
+  pluginConfig: {
+    captureScalars: true
+  }
 })
 
 function createKoaSegmentsTests(t, frameworkName) {
diff --git a/tests/versioned/apollo-server/segments.test.js b/tests/versioned/apollo-server/segments.test.js
index 491ed93..9cca53f 100644
--- a/tests/versioned/apollo-server/segments.test.js
+++ b/tests/versioned/apollo-server/segments.test.js
@@ -6,6 +6,8 @@
 'use strict'
 
 const { setupApolloServerTests } = require('../../apollo-server-setup')
-const expressSegmentsTests = require('../express-segments-tests')
+const expressSegmentsDefaultTests = require('../express-segments-default-tests')
+const expressSegmentsScalarTests = require('../express-segments-scalar-tests')
 
-setupApolloServerTests(expressSegmentsTests)
+setupApolloServerTests(expressSegmentsDefaultTests)
+setupApolloServerTests(expressSegmentsScalarTests)
diff --git a/tests/versioned/express-segments-default-tests.js b/tests/versioned/express-segments-default-tests.js
new file mode 100644
index 0000000..82ccfb2
--- /dev/null
+++ b/tests/versioned/express-segments-default-tests.js
@@ -0,0 +1,653 @@
+/*
+ * Copyright 2020 New Relic Corporation. All rights reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+'use strict'
+
+const { executeQuery, executeQueryBatch } = require('../test-client')
+
+const ANON_PLACEHOLDER = '<anonymous>'
+const UNKNOWN_OPERATION = '<unknown>'
+
+const OPERATION_PREFIX = 'GraphQL/operation/ApolloServer'
+const RESOLVE_PREFIX = 'GraphQL/resolve/ApolloServer'
+
+/**
+ * Creates a set of standard segment naming and nesting tests to run
+ * against express-based apollo-server libraries.
+ * It is required that t.context.helper and t.context.serverUrl are set.
+ * @param {*} t a tap test instance
+ */
+function createSegmentsTests(t, frameworkName) {
+  const TRANSACTION_PREFIX = `WebTransaction/${frameworkName}/POST`
+
+  t.test('anonymous query, single level', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const query = `query {
+      hello
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${ANON_PLACEHOLDER}/hello`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [{
+                name: `${RESOLVE_PREFIX}/hello`
+              }]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named query, single level', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'HeyThere'
+    const query = `query ${expectedName} {
+      hello
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/hello`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [{
+                name: `${RESOLVE_PREFIX}/hello`
+              }]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('anonymous query, multi-level', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const query = `query {
+      libraries {
+        books {
+          title
+          author {
+            name
+          }
+        }
+      }
+    }`
+
+    const deepestPath = 'libraries.books.author.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${ANON_PLACEHOLDER}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                { name: `${RESOLVE_PREFIX}/libraries` },
+                { name: `${RESOLVE_PREFIX}/libraries.books` },
+                { name: `${RESOLVE_PREFIX}/libraries.books.author` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named query, multi-level should return deepest path', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'GetBooksByLibrary'
+    const query = `query ${expectedName} {
+      libraries {
+        books {
+          title
+          author {
+            name
+          }
+        }
+      }
+    }`
+
+    const deepestPath = 'libraries.books.author.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                { name: `${RESOLVE_PREFIX}/libraries` },
+                { name: `${RESOLVE_PREFIX}/libraries.books` },
+                { name: `${RESOLVE_PREFIX}/libraries.books.author` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named query, multi-level, should choose *first* deepest-path', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'GetBooksByLibrary'
+    const query = `query ${expectedName} {
+      libraries {
+        books {
+          title
+          isbn
+        }
+      }
+    }`
+
+    // .isbn is the same length but title will be first so that path should be used
+    const firstDeepestPath = 'libraries.books.title'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/${firstDeepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                { name: `${RESOLVE_PREFIX}/libraries` },
+                { name: `${RESOLVE_PREFIX}/libraries.books` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('anonymous mutation, single level', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const query = `mutation {
+      addThing(name: "added thing!")
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `mutation/${ANON_PLACEHOLDER}/addThing`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [{
+                name: `${RESOLVE_PREFIX}/addThing`,
+                children: [{
+                  name: 'timers.setTimeout',
+                  children: [{
+                    name: 'Callback: namedCallback'
+                  }]
+                }]
+              }]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named mutation, single level, should use mutation name', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'AddThing'
+    const query = `mutation ${expectedName} {
+      addThing(name: "added thing!")
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `mutation/${expectedName}/addThing`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [{
+                name: `${RESOLVE_PREFIX}/addThing`,
+                children: [{
+                  name: 'timers.setTimeout',
+                  children: [{
+                    name: 'Callback: namedCallback'
+                  }]
+                }]
+              }]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('anonymous query, with params', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const query = `query {
+      paramQuery(blah: "blah", blee: "blee")
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${ANON_PLACEHOLDER}/paramQuery`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                { name: `${RESOLVE_PREFIX}/paramQuery` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named query, with params', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'BlahQuery'
+    const query = `query ${expectedName} {
+      paramQuery(blah: "blah")
+    }`
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/paramQuery`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                { name: `${RESOLVE_PREFIX}/paramQuery` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('named query, with params, multi-level', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName = 'GetBookForLibrary'
+    const query = `query ${expectedName} {
+      library(branch: "downtown") {
+        books {
+          title
+          author {
+            name
+          }
+        }
+      }
+    }`
+
+    const deepestPath = 'library.books.author.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${expectedName}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`,
+              children: [
+                {
+                  name: `${RESOLVE_PREFIX}/library`,
+                  children: [{
+                    name: 'timers.setTimeout',
+                    children: [{
+                      name: 'Callback: <anonymous>'
+                    }]
+                  }]
+                },
+                { name: `${RESOLVE_PREFIX}/library.books` },
+                { name: `${RESOLVE_PREFIX}/library.books.author` }
+              ]
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, query, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.end()
+      })
+    })
+  })
+
+  t.test('batch query should include segments for nested queries', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const expectedName1 = 'GetBookForLibrary'
+    const query1 = `query ${expectedName1} {
+      library(branch: "downtown") {
+        books {
+          title
+          author {
+            name
+          }
+        }
+      }
+    }`
+
+    const query2 = `mutation {
+      addThing(name: "added thing!")
+    }`
+
+    const deepestPath1 = 'library.books.author.name'
+
+    const queries = [query1, query2]
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart1 = `query/${expectedName1}/${deepestPath1}`
+      const expectedQuery1Name = `${operationPart1}`
+      const operationPart2 = `mutation/${ANON_PLACEHOLDER}/addThing`
+      const expectedQuery2Name = `${operationPart2}`
+
+      const batchTransactionPrefix = `${TRANSACTION_PREFIX}//batch`
+
+      const expectedSegments = [{
+        name: `${batchTransactionPrefix}/${expectedQuery1Name}/${expectedQuery2Name}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [
+              {
+                name: `${OPERATION_PREFIX}/${operationPart1}`,
+                children: [
+                  {
+                    name: `${RESOLVE_PREFIX}/library`,
+                    children: [{
+                      name: 'timers.setTimeout',
+                      children: [{
+                        name: 'Callback: <anonymous>'
+                      }]
+                    }]
+                  },
+                  { name: `${RESOLVE_PREFIX}/library.books` },
+                  { name: `${RESOLVE_PREFIX}/library.books.author` }
+                ]
+              },
+              {
+                name: `${OPERATION_PREFIX}/${operationPart2}`,
+                children: [{
+                  name: `${RESOLVE_PREFIX}/addThing`,
+                  children: [{
+                    name: 'timers.setTimeout',
+                    children: [{
+                      name: 'Callback: namedCallback'
+                    }]
+                  }]
+                }]
+              }
+            ]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQueryBatch(serverUrl, queries, (err, result) => {
+      t.error(err)
+      checkResult(t, result, () => {
+        t.equal(result.length, 2)
+
+        t.end()
+      })
+    })
+  })
+
+  // there will be no document/AST nor resolved operation
+  t.test('when the query cannot be parsed, should have operation placeholder', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const invalidQuery = `query {
+      libraries {
+        books {
+          title
+          author {
+            name
+          }
+        }
+      }
+    ` // missing closing }
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//*`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${UNKNOWN_OPERATION}`
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, invalidQuery, (err, result) => {
+      t.error(err)
+
+      t.ok(result)
+      t.ok(result.errors)
+      t.equal(result.errors.length, 1) // should have one parsing error
+
+      const [parseError] = result.errors
+      t.equal(parseError.extensions.code, 'GRAPHQL_PARSE_FAILED')
+
+      t.end()
+    })
+  })
+
+  // if parse succeeds but validation fails, there will not be a resolved operation
+  // but the document/AST can still be leveraged for what was intended.
+  t.test('when cannot validate, should include operation segment', (t) => {
+    const { helper, serverUrl } = t.context
+
+    const invalidQuery = `query {
+      libraries {
+        books {
+          title
+          doesnotexist {
+            name
+          }
+        }
+      }
+    }`
+
+    const deepestPath = 'libraries.books.doesnotexist.name'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const operationPart = `query/${ANON_PLACEHOLDER}/${deepestPath}`
+      const expectedSegments = [{
+        name: `${TRANSACTION_PREFIX}//${operationPart}`,
+        children: [{
+          name: 'Expressjs/Router: /',
+          children: [{
+            name: 'Nodejs/Middleware/Expressjs/<anonymous>',
+            children: [{
+              name: `${OPERATION_PREFIX}/${operationPart}`
+            }]
+          }]
+        }]
+      }]
+
+      t.segments(transaction.trace.root, expectedSegments)
+    })
+
+    executeQuery(serverUrl, invalidQuery, (err, result) => {
+      t.error(err)
+
+      t.ok(result)
+      t.ok(result.errors)
+      t.equal(result.errors.length, 1) // should have one parsing error
+
+      const [parseError] = result.errors
+      t.equal(parseError.extensions.code, 'GRAPHQL_VALIDATION_FAILED')
+
+      t.end()
+    })
+  })
+}
+
+/**
+ * Verify we didn't break anything outright and
+ * test is setup correctly for functioning calls.
+ */
+function checkResult(t, result, callback) {
+  t.ok(result)
+
+  if (result.errors) {
+    result.errors.forEach((error) => {
+      t.error(error)
+    })
+  }
+
+  setImmediate(callback)
+}
+
+module.exports = {
+  suiteName: 'express segments default configuration',
+  createTests: createSegmentsTests
+}
diff --git a/tests/versioned/express-segments-tests.js b/tests/versioned/express-segments-scalar-tests.js
similarity index 99%
rename from tests/versioned/express-segments-tests.js
rename to tests/versioned/express-segments-scalar-tests.js
index fa9f64b..fe94157 100644
--- a/tests/versioned/express-segments-tests.js
+++ b/tests/versioned/express-segments-scalar-tests.js
@@ -657,6 +657,7 @@ function checkResult(t, result, callback) {
 }
 
 module.exports = {
-  testSuite: 'express segments',
-  createTests: createSegmentsTests
+  suiteName: 'express segments with scalars',
+  createTests: createSegmentsTests,
+  pluginConfig: { captureScalars: true }
 }