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

Issue:
Internal server error for some graphql-upload requests
## Description

We are currently using the graphql-upload package to add upload support to our graphql api, however for certain upload mutations we are receiving the error below which prevents the upload from succeeding. Disabling the newrelic addin solves the issue however that isn't a solution we want to consider in our production environment.

**Example request**
```
POST https://xxxxx.com/ HTTP/1.1
Content-Type: multipart/form-data; boundary="ef0e2df4-f746-4871-a349-4634341a5a24"
Host: xxxx.com
Content-Length: 131811
Expect: 100-continue

--ef0e2df4-f746-4871-a349-4634341a5a24
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="operations"


{
    "operationName": "uploadDocument",
    "query": "\r\nmutation($file: Upload!) \r\n{\r\n    upload(input : {file: $file})\r\n    {\r\n        code, \r\n        error,\r\n        message,\r\n        uniqueId\r\n    }\r\n}"
}

--ef0e2df4-f746-4871-a349-4634341a5a24
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name="map"

{"file":["variables.file"] }
--ef0e2df4-f746-4871-a349-4634341a5a24
Content-Disposition: form-data; name="file"; filename="TST-101349-1384.pdf"
Content-Type: application/pdf

%PDF-1.4
%    
...
```

**Errror**
```
{
  "errors": [
    {
      "message": "Cannot read properties of undefined (reading 'selectionSet')",
      "extensions":
      {
        "code": "INTERNAL_SERVER_ERROR",
        "exception":
        {
          "stacktrace": ["TypeError: Cannot read properties of undefined (reading 'selectionSet')", "    at isIntrospectionQuery (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:461:20)", "    at shouldIgnoreTransaction (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:476:46)", "    at Object.didResolveOperation (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:105:15)", "    at /node_modules/apollo-server-core/dist/utils/dispatcher.js:21:31", "    at Array.map (<anonymous>)", "    at Dispatcher.callTargets (/node_modules/apollo-server-core/dist/utils/dispatcher.js:18:24)", "    at Dispatcher.<anonymous> (/node_modules/apollo-server-core/dist/utils/dispatcher.js:27:43)", "    at Generator.next (<anonymous>)", "    at /node_modules/apollo-server-core/dist/utils/dispatcher.js:8:71", "    at new Promise (<anonymous>)"]
        }
      }
    }
  ]
}
```

## Expected Behavior

The newrelic plugin shouldn't throw an error - it should ensure that any errors are captured and logged and not bubbled up to the calling application.

## Steps to Reproduce

See steps above in the description

## Your Environment

node 16.15
"@newrelic/apollo-server-plugin": "^1.3.0",
"apollo-server": "^3.8.1",
"graphql": "^15.5.0",
graphql-upload": "^13.0.0",
"newrelic": "^8.12.0",

## Additional context

I have raised a PR to prevent the error from occurring, see: 
https://github.com/newrelic/newrelic-node-apollo-server-plugin/pull/205


Pull request description:
handle undefined operation
<!--
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
 * Added defensive code to prevent plugin from masking actual graphql errors.

## Links
Closes #206 


## Details
We are currently using the newrelic addin for apollo graphql along with the graphql-upload package to support file uploads via our graphql api.

For some upload requests we are receiving an error from the newrelic plugin that is preventing the upload request from continuing/completing.

Upon investigating the issue, is seems to be caused due to the "operation" being undefined within the newrelic plugin for these requests. The current plugin code doesn't have guards to consider this scenario resulting in an unhandled error.

**Example GQL Request:**
```
mutation($file: Upload!)
{
    upload(input : {file: $file})
    {
        code,
        error,
        message,
        uniqueId
    }
}
```
**Example Response**
```
{
  "errors": [
    {
      "message": "Cannot read properties of undefined (reading 'selectionSet')",
      "extensions":
      {
        "code": "INTERNAL_SERVER_ERROR",
        "exception":
        {
          "stacktrace": ["TypeError: Cannot read properties of undefined (reading 'selectionSet')", "    at isIntrospectionQuery (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:461:20)", "    at shouldIgnoreTransaction (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:476:46)", "    at Object.didResolveOperation (/node_modules/@newrelic/apollo-server-plugin/lib/create-plugin.js:105:15)", "    at /node_modules/apollo-server-core/dist/utils/dispatcher.js:21:31", "    at Array.map (<anonymous>)", "    at Dispatcher.callTargets (/node_modules/apollo-server-core/dist/utils/dispatcher.js:18:24)", "    at Dispatcher.<anonymous> (/node_modules/apollo-server-core/dist/utils/dispatcher.js:27:43)", "    at Generator.next (<anonymous>)", "    at /node_modules/apollo-server-core/dist/utils/dispatcher.js:8:71", "    at new Promise (<anonymous>)"]
        }
      }
    }
  ]
}
```

Patch:
diff --git a/lib/create-plugin.js b/lib/create-plugin.js
index e3aa28a..cdf80ae 100644
--- a/lib/create-plugin.js
+++ b/lib/create-plugin.js
@@ -473,6 +473,11 @@ function isHealthCheckQuery(operation) {
 }
 
 function shouldIgnoreTransaction(operation, config, logger) {
+  if (!operation) {
+    logger.trace('`operation` is undefined. Skipping query type check.')
+    return false
+  }
+
   if (!config.captureIntrospectionQueries && isIntrospectionQuery(operation)) {
     logger.trace(
       'Request is an introspection query and ' +


Test patch:
diff --git a/tests/unit/create-plugin.test.js b/tests/unit/create-plugin.test.js
index 6988d0c..31fd080 100644
--- a/tests/unit/create-plugin.test.js
+++ b/tests/unit/create-plugin.test.js
@@ -83,4 +83,13 @@ tap.test('createPlugin edge cases', (t) => {
     )
     t.end()
   })
+
+  t.test('should not crash when ctx.operation is undefined in didResolveOperation', (t) => {
+    const hooks = createPlugin(instrumentationApi)
+    const operationHooks = hooks.requestDidStart({})
+    t.doesNotThrow(() => {
+      operationHooks.didResolveOperation({})
+    })
+    t.end()
+  })
 })
diff --git a/tests/versioned/errors-tests.js b/tests/versioned/errors-tests.js
index e159d25..44a051f 100644
--- a/tests/versioned/errors-tests.js
+++ b/tests/versioned/errors-tests.js
@@ -5,7 +5,7 @@
 
 'use strict'
 
-const { executeQuery } = require('../test-client')
+const { executeQuery, makeRequest } = require('../test-client')
 const agentTesting = require('../agent-testing')
 
 const ANON_PLACEHOLDER = '<anonymous>'
@@ -244,6 +244,46 @@ function createErrorTests(t) {
       })
     })
   })
+
+  t.test('Invalid operation name should not crash server', (t) => {
+    const { helper, serverUrl } = t.context
+    const query = 'query Hello { hello }'
+    const expectedErrorMessage = 'Unknown operation named "testMe".'
+    const expectedErrorType = 'GraphQLError'
+
+    helper.agent.on('transactionFinished', (transaction) => {
+      const errorTraces = agentTesting.getErrorTraces(helper.agent)
+      t.equal(errorTraces.length, 1)
+
+      const errorTrace = errorTraces[0]
+
+      const [, transactionName, errorMessage, errorType, params] = errorTrace
+      t.equal(transactionName, transaction.name)
+      t.equal(errorMessage, expectedErrorMessage)
+      t.equal(errorType, expectedErrorType)
+
+      const { agentAttributes } = params
+
+      t.ok(agentAttributes.spanId)
+
+      const matchingSpan = agentTesting.findSpanById(helper.agent, agentAttributes.spanId)
+
+      const { attributes } = matchingSpan
+      t.equal(attributes['error.message'], expectedErrorMessage)
+      t.equal(attributes['error.class'], expectedErrorType)
+    })
+    const data = JSON.stringify({ query, operationName: 'testMe' })
+    makeRequest(serverUrl, data, (err, result) => {
+      t.error(err)
+      t.ok(result)
+      t.ok(result.errors)
+      t.equal(result.errors.length, 1) // should have one parsing error
+      const [resolverError] = result.errors
+      t.equal(resolverError.extensions.code, 'INTERNAL_SERVER_ERROR')
+      t.equal(resolverError.message, expectedErrorMessage)
+      t.end()
+    })
+  })
 }
 
 module.exports = {