@envelop/newrelic 1.0.3-alpha-52fc7d2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/index.d.ts +11 -0
- package/index.js +169 -0
- package/index.mjs +163 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
## `@envelop/newrelic`
|
|
2
|
+
|
|
3
|
+
Instrument your GraphQL application with New Relic reporting.
|
|
4
|
+
Take advantage of Distributed tracing to monitor performance and errors whilst ultimately getting to the root cause of issues.
|
|
5
|
+
|
|
6
|
+
Below are some screenshots to show how tracking looks like in New Relic, in these examples all plugin options were set to `true`.
|
|
7
|
+
|
|
8
|
+
Error tracking, operation and resolver views:
|
|
9
|
+

|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
Successful operation tracking, operation plus root-field and sub-field resolvers:
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
1. Install dependencies
|
|
20
|
+
2. Configure the New Relic Agent
|
|
21
|
+
3. Setup the New Relic Envelop plugin
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
This plugin expects the Node.js agent, [newrelic npm package](https://www.npmjs.com/package/newrelic), to be installed in your application.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
yarn add newrelic @envelop/newrelic
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic usage Example
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { envelop } from '@envelop/core';
|
|
35
|
+
import { useNewRelic } from '@envelop/newrelic';
|
|
36
|
+
|
|
37
|
+
const getEnveloped = envelop({
|
|
38
|
+
plugins: [
|
|
39
|
+
// ... other plugins ...
|
|
40
|
+
useNewRelic({
|
|
41
|
+
includeOperationDocument: true, // default `false`. When set to `true`, includes the GraphQL document defining the operations and fragments
|
|
42
|
+
includeExecuteVariables: false, // default `false`. When set to `true`, includes all the operation variables with their values
|
|
43
|
+
includeRawResult: false, // default: `false`. When set to `true`, includes the execution result
|
|
44
|
+
trackResolvers: true, // default `false`. When set to `true`, track resolvers as segments to monitor their performance
|
|
45
|
+
includeResolverArgs: false, // default `false`. When set to `true`, includes all the arguments passed to resolvers with their values
|
|
46
|
+
rootFieldsNaming: true, // default `false`. When set to `true` append the names of operation root fields to the transaction name
|
|
47
|
+
operationNameProperty: 'id', // default empty. When passed will check for the property name passed, within the document object. Will eventually use its value as operation name. Useful for custom document properties (e.g. queryId/hash)
|
|
48
|
+
}),
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> Note: Transaction and segment/span timings may be affected by other plugins used. In order to get more accurate tracking, it is recommended to add the New Relic plugin last.
|
|
54
|
+
|
|
55
|
+
## Advanced usage
|
|
56
|
+
|
|
57
|
+
The plugin allows you to keep control over the variables and arguments that are tracked in New Relic.
|
|
58
|
+
In addition to the basic `true/false` boolean value, `includeExecuteVariables` and `includeResolverArgs` also accept a RegEx pattern. This allows you to implement white and black listing of properties to be tracked in New Relic.
|
|
59
|
+
|
|
60
|
+
This is particularly useful if you have properties coming through variables and arguments that are useful for debugging, but you don't want to leak users' data (such as PII).
|
|
61
|
+
Below is a quick example of how you can use RegEx to set up white/black listing functionalities.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
useNewRelic({
|
|
65
|
+
includeExecuteVariables: /^(client.*|application.*)$.*$/i, // whitelist, track only variables whose name starts with client or application (e.g. clientName, applicationId)
|
|
66
|
+
trackResolvers: true, // track resolvers, since we also want to track resolvers' arguments
|
|
67
|
+
includeResolverArgs: /^(?!(name|email|password)$).*$/, // blacklist, track all arguments whose name does not match 'name', 'email' nor 'password'
|
|
68
|
+
}),
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Obviously, the ones above are just a couple of examples, but clearly you have endless options to use any RegEx pattern to filter the variables and arguments in the way that best work for you.
|
|
72
|
+
|
|
73
|
+
> Be aware that the only way to filter variables and arguments is to loop through them, so there is an O(_n_) cost when filtering, where _n_ is the number of variables sent to the operation (when tracking execute variables), or the number of arguments passed to resolvers (when tracking resolvers arguments).
|
|
74
|
+
|
|
75
|
+
## Agent Configuration
|
|
76
|
+
|
|
77
|
+
For full details about New Relic Agent configuration, we recommend reading [New Relic's official Node.js agent configuration documentation](https://docs.newrelic.com/docs/agents/nodejs-agent/installation-configuration/nodejs-agent-configuration/).
|
|
78
|
+
|
|
79
|
+
The main methods to configure the New Relic Agent is through:
|
|
80
|
+
|
|
81
|
+
- The newrelic.js file [or]
|
|
82
|
+
- The environment variables
|
|
83
|
+
|
|
84
|
+
If you choose to use the newrelic.js file, then you need this file located in the root of your application. You can look [here](https://github.com/newrelic/node-newrelic/blob/main/newrelic.js) for a basic example of what this file can look like.
|
|
85
|
+
|
|
86
|
+
If you choose to configure the New Relic Agent through environment variables, then you can follow your preferred strategy to make sure the variables are set and available when your application starts.
|
|
87
|
+
The variables are the same you can set in newrelic.js file, you just need to know that they need to start with `NEW_RELIC_`, obviously, the variables name must be fully uppercase.
|
|
88
|
+
|
|
89
|
+
The two variables that are always required are:
|
|
90
|
+
|
|
91
|
+
| Description | newrelic.js | Environment variable |
|
|
92
|
+
| ---------------- | ---------------------------------------- | ----------------------------------------------- |
|
|
93
|
+
| Application name | `app_name: ['MyAppName']` | `NEW_RELIC_APP_NAME=MyAppName` |
|
|
94
|
+
| License key | `license_key: '40HexadecimalCharacters'` | `NEW_RELIC_LICENSE_KEY=40HexadecimalCharacters` |
|
|
95
|
+
|
|
96
|
+
Other variables that are popularly used are:
|
|
97
|
+
|
|
98
|
+
| Description | newrelic.js | Environment variable |
|
|
99
|
+
| --------------------------- | ---------------------------------------- | -------------------------------------------- |
|
|
100
|
+
| Enable distributed tracing | `distributed_tracing: { enabled: true }` | `NEW_RELIC_DISTRIBUTED_TRACING_ENABLED=true` |
|
|
101
|
+
| Logging level | `logging: { level: 'info' }` | `NEW_RELIC_LOG_LEVEL=info` |
|
|
102
|
+
| Capture all request headers | `allow_all_headers: true` | `NEW_RELIC_ALLOW_ALL_HEADERS=true` |
|
|
103
|
+
| Enable error collection | `error_collector: { enabled: true }` | `NEW_RELIC_ERROR_COLLECTOR_ENABLED=true` |
|
|
104
|
+
|
|
105
|
+
Finally, [here](https://github.com/newrelic/node-newrelic/blob/main/lib/config/default.js) is also a reference of all the configuration variables you can include in your newrelic.js file, or translate into the env variables equivalent.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Plugin } from '@envelop/types';
|
|
2
|
+
export declare type UseNewRelicOptions = {
|
|
3
|
+
includeOperationDocument?: boolean;
|
|
4
|
+
includeExecuteVariables?: boolean | RegExp;
|
|
5
|
+
includeRawResult?: boolean;
|
|
6
|
+
trackResolvers?: boolean;
|
|
7
|
+
includeResolverArgs?: boolean | RegExp;
|
|
8
|
+
rootFieldsNaming?: boolean;
|
|
9
|
+
operationNameProperty?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const useNewRelic: (rawOptions?: UseNewRelicOptions | undefined) => Plugin;
|
package/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
|
|
6
|
+
|
|
7
|
+
const newRelic = _interopDefault(require('newrelic'));
|
|
8
|
+
const graphql = require('graphql');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if the provided object implements the AsyncIterator protocol via
|
|
12
|
+
* implementing a `Symbol.asyncIterator` method.
|
|
13
|
+
*
|
|
14
|
+
* Source: https://github.com/graphql/graphql-js/blob/main/src/jsutils/isAsyncIterable.ts
|
|
15
|
+
*/
|
|
16
|
+
function isAsyncIterable(maybeAsyncIterable) {
|
|
17
|
+
return typeof (maybeAsyncIterable === null || maybeAsyncIterable === void 0 ? void 0 : maybeAsyncIterable[Symbol.asyncIterator]) === 'function';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { shim: instrumentationApi } = newRelic;
|
|
21
|
+
var AttributeName;
|
|
22
|
+
(function (AttributeName) {
|
|
23
|
+
AttributeName["COMPONENT_NAME"] = "Envelop_NewRelic_Plugin";
|
|
24
|
+
AttributeName["ANONYMOUS_OPERATION"] = "<anonymous>";
|
|
25
|
+
AttributeName["EXECUTION_RESULT"] = "graphql.execute.result";
|
|
26
|
+
AttributeName["EXECUTION_OPERATION_NAME"] = "graphql.execute.operationName";
|
|
27
|
+
AttributeName["EXECUTION_OPERATION_TYPE"] = "graphql.execute.operationType";
|
|
28
|
+
AttributeName["EXECUTION_OPERATION_DOCUMENT"] = "graphql.execute.document";
|
|
29
|
+
AttributeName["EXECUTION_VARIABLES"] = "graphql.execute.variables";
|
|
30
|
+
AttributeName["RESOLVER_FIELD_PATH"] = "graphql.resolver.fieldPath";
|
|
31
|
+
AttributeName["RESOLVER_TYPE_NAME"] = "graphql.resolver.typeName";
|
|
32
|
+
AttributeName["RESOLVER_RESULT_TYPE"] = "graphql.resolver.resultType";
|
|
33
|
+
AttributeName["RESOLVER_RESULT"] = "graphql.resolver.result";
|
|
34
|
+
AttributeName["RESOLVER_ARGS"] = "graphql.resolver.args";
|
|
35
|
+
})(AttributeName || (AttributeName = {}));
|
|
36
|
+
const DEFAULT_OPTIONS = {
|
|
37
|
+
includeOperationDocument: false,
|
|
38
|
+
includeExecuteVariables: false,
|
|
39
|
+
includeRawResult: false,
|
|
40
|
+
trackResolvers: false,
|
|
41
|
+
includeResolverArgs: false,
|
|
42
|
+
rootFieldsNaming: false,
|
|
43
|
+
operationNameProperty: '',
|
|
44
|
+
};
|
|
45
|
+
const useNewRelic = (rawOptions) => {
|
|
46
|
+
const options = {
|
|
47
|
+
...DEFAULT_OPTIONS,
|
|
48
|
+
...(rawOptions || {}),
|
|
49
|
+
};
|
|
50
|
+
options.isExecuteVariablesRegex = options.includeExecuteVariables instanceof RegExp;
|
|
51
|
+
options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp;
|
|
52
|
+
const logger = instrumentationApi.logger.child({ component: AttributeName.COMPONENT_NAME });
|
|
53
|
+
logger.info(`${AttributeName.COMPONENT_NAME} registered`);
|
|
54
|
+
instrumentationApi.agent.metrics
|
|
55
|
+
.getOrCreateMetric(`Supportability/ExternalModules/${AttributeName.COMPONENT_NAME}`)
|
|
56
|
+
.incrementCallCount();
|
|
57
|
+
return {
|
|
58
|
+
onExecute({ args }) {
|
|
59
|
+
var _a;
|
|
60
|
+
const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState;
|
|
61
|
+
const spanContext = instrumentationApi.agent.tracer.getSpanContext();
|
|
62
|
+
const delimiter = transactionNameState.delimiter;
|
|
63
|
+
const rootOperation = args.document.definitions.find(definitionNode => definitionNode.kind === graphql.Kind.OPERATION_DEFINITION);
|
|
64
|
+
const operationType = rootOperation.operation;
|
|
65
|
+
const document = graphql.print(args.document);
|
|
66
|
+
const operationName = args.document[options.operationNameProperty] ||
|
|
67
|
+
args.operationName ||
|
|
68
|
+
((_a = rootOperation.name) === null || _a === void 0 ? void 0 : _a.value) ||
|
|
69
|
+
AttributeName.ANONYMOUS_OPERATION;
|
|
70
|
+
let rootFields = null;
|
|
71
|
+
if (options.rootFieldsNaming) {
|
|
72
|
+
const fieldNodes = rootOperation.selectionSet.selections.filter(selectionNode => selectionNode.kind === graphql.Kind.FIELD);
|
|
73
|
+
rootFields = fieldNodes.map(fieldNode => fieldNode.name.value);
|
|
74
|
+
}
|
|
75
|
+
transactionNameState.setName(transactionNameState.prefix, transactionNameState.verb, delimiter, operationType + delimiter + operationName + (rootFields ? delimiter + rootFields.join('&') : ''));
|
|
76
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_NAME, operationName);
|
|
77
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_TYPE, operationType);
|
|
78
|
+
options.includeOperationDocument && spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_DOCUMENT, document);
|
|
79
|
+
if (options.includeExecuteVariables) {
|
|
80
|
+
const rawVariables = args.variableValues || {};
|
|
81
|
+
const executeVariablesToTrack = options.isExecuteVariablesRegex
|
|
82
|
+
? filterPropertiesByRegex(rawVariables, options.includeExecuteVariables)
|
|
83
|
+
: rawVariables;
|
|
84
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_VARIABLES, JSON.stringify(executeVariablesToTrack));
|
|
85
|
+
}
|
|
86
|
+
const operationSegment = instrumentationApi.getActiveSegment();
|
|
87
|
+
const onResolverCalled = options.trackResolvers
|
|
88
|
+
? ({ args: resolversArgs, info }) => {
|
|
89
|
+
const { returnType, path, parentType } = info;
|
|
90
|
+
const formattedPath = flattenPath(path, delimiter);
|
|
91
|
+
const currentSegment = instrumentationApi.getActiveSegment();
|
|
92
|
+
if (!currentSegment) {
|
|
93
|
+
logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath);
|
|
94
|
+
return () => { };
|
|
95
|
+
}
|
|
96
|
+
const resolverSegment = instrumentationApi.createSegment(`resolver${delimiter}${formattedPath}`, null, operationSegment);
|
|
97
|
+
if (!resolverSegment) {
|
|
98
|
+
logger.trace('Resolver segment was not created (%s).', formattedPath);
|
|
99
|
+
return () => { };
|
|
100
|
+
}
|
|
101
|
+
resolverSegment.start();
|
|
102
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath);
|
|
103
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString());
|
|
104
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString());
|
|
105
|
+
if (options.includeResolverArgs) {
|
|
106
|
+
const rawArgs = resolversArgs || {};
|
|
107
|
+
const resolverArgsToTrack = options.isResolverArgsRegex
|
|
108
|
+
? filterPropertiesByRegex(rawArgs, options.includeResolverArgs)
|
|
109
|
+
: rawArgs;
|
|
110
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack));
|
|
111
|
+
}
|
|
112
|
+
return ({ result }) => {
|
|
113
|
+
if (options.includeRawResult) {
|
|
114
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result));
|
|
115
|
+
}
|
|
116
|
+
if (result instanceof Error) {
|
|
117
|
+
const transaction = instrumentationApi.tracer.getTransaction();
|
|
118
|
+
instrumentationApi.agent.errors.add(transaction, JSON.stringify(result));
|
|
119
|
+
}
|
|
120
|
+
resolverSegment.end();
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
: undefined;
|
|
124
|
+
return {
|
|
125
|
+
onResolverCalled,
|
|
126
|
+
onExecuteDone({ result }) {
|
|
127
|
+
if (isAsyncIterable(result)) {
|
|
128
|
+
operationSegment.end();
|
|
129
|
+
// eslint-disable-next-line no-console
|
|
130
|
+
console.warn(`Plugin "newrelic" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (result.data && options.includeRawResult) {
|
|
134
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_RESULT, JSON.stringify(result));
|
|
135
|
+
}
|
|
136
|
+
if (result.errors && result.errors.length > 0) {
|
|
137
|
+
const agent = instrumentationApi.agent;
|
|
138
|
+
const transaction = instrumentationApi.tracer.getTransaction();
|
|
139
|
+
for (const error of result.errors) {
|
|
140
|
+
agent.errors.add(transaction, JSON.stringify(error));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
operationSegment.end();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
function flattenPath(fieldPath, delimiter = '/') {
|
|
150
|
+
const pathArray = [];
|
|
151
|
+
let thisPath = fieldPath;
|
|
152
|
+
while (thisPath) {
|
|
153
|
+
if (typeof thisPath.key !== 'number') {
|
|
154
|
+
pathArray.push(thisPath.key);
|
|
155
|
+
}
|
|
156
|
+
thisPath = thisPath.prev;
|
|
157
|
+
}
|
|
158
|
+
return pathArray.reverse().join(delimiter);
|
|
159
|
+
}
|
|
160
|
+
function filterPropertiesByRegex(initialObject, pattern) {
|
|
161
|
+
const filteredObject = {};
|
|
162
|
+
for (const property of Object.keys(initialObject)) {
|
|
163
|
+
if (pattern.test(property))
|
|
164
|
+
filteredObject[property] = initialObject[property];
|
|
165
|
+
}
|
|
166
|
+
return filteredObject;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
exports.useNewRelic = useNewRelic;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import newRelic from 'newrelic';
|
|
2
|
+
import { Kind, print } from 'graphql';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if the provided object implements the AsyncIterator protocol via
|
|
6
|
+
* implementing a `Symbol.asyncIterator` method.
|
|
7
|
+
*
|
|
8
|
+
* Source: https://github.com/graphql/graphql-js/blob/main/src/jsutils/isAsyncIterable.ts
|
|
9
|
+
*/
|
|
10
|
+
function isAsyncIterable(maybeAsyncIterable) {
|
|
11
|
+
return typeof (maybeAsyncIterable === null || maybeAsyncIterable === void 0 ? void 0 : maybeAsyncIterable[Symbol.asyncIterator]) === 'function';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { shim: instrumentationApi } = newRelic;
|
|
15
|
+
var AttributeName;
|
|
16
|
+
(function (AttributeName) {
|
|
17
|
+
AttributeName["COMPONENT_NAME"] = "Envelop_NewRelic_Plugin";
|
|
18
|
+
AttributeName["ANONYMOUS_OPERATION"] = "<anonymous>";
|
|
19
|
+
AttributeName["EXECUTION_RESULT"] = "graphql.execute.result";
|
|
20
|
+
AttributeName["EXECUTION_OPERATION_NAME"] = "graphql.execute.operationName";
|
|
21
|
+
AttributeName["EXECUTION_OPERATION_TYPE"] = "graphql.execute.operationType";
|
|
22
|
+
AttributeName["EXECUTION_OPERATION_DOCUMENT"] = "graphql.execute.document";
|
|
23
|
+
AttributeName["EXECUTION_VARIABLES"] = "graphql.execute.variables";
|
|
24
|
+
AttributeName["RESOLVER_FIELD_PATH"] = "graphql.resolver.fieldPath";
|
|
25
|
+
AttributeName["RESOLVER_TYPE_NAME"] = "graphql.resolver.typeName";
|
|
26
|
+
AttributeName["RESOLVER_RESULT_TYPE"] = "graphql.resolver.resultType";
|
|
27
|
+
AttributeName["RESOLVER_RESULT"] = "graphql.resolver.result";
|
|
28
|
+
AttributeName["RESOLVER_ARGS"] = "graphql.resolver.args";
|
|
29
|
+
})(AttributeName || (AttributeName = {}));
|
|
30
|
+
const DEFAULT_OPTIONS = {
|
|
31
|
+
includeOperationDocument: false,
|
|
32
|
+
includeExecuteVariables: false,
|
|
33
|
+
includeRawResult: false,
|
|
34
|
+
trackResolvers: false,
|
|
35
|
+
includeResolverArgs: false,
|
|
36
|
+
rootFieldsNaming: false,
|
|
37
|
+
operationNameProperty: '',
|
|
38
|
+
};
|
|
39
|
+
const useNewRelic = (rawOptions) => {
|
|
40
|
+
const options = {
|
|
41
|
+
...DEFAULT_OPTIONS,
|
|
42
|
+
...(rawOptions || {}),
|
|
43
|
+
};
|
|
44
|
+
options.isExecuteVariablesRegex = options.includeExecuteVariables instanceof RegExp;
|
|
45
|
+
options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp;
|
|
46
|
+
const logger = instrumentationApi.logger.child({ component: AttributeName.COMPONENT_NAME });
|
|
47
|
+
logger.info(`${AttributeName.COMPONENT_NAME} registered`);
|
|
48
|
+
instrumentationApi.agent.metrics
|
|
49
|
+
.getOrCreateMetric(`Supportability/ExternalModules/${AttributeName.COMPONENT_NAME}`)
|
|
50
|
+
.incrementCallCount();
|
|
51
|
+
return {
|
|
52
|
+
onExecute({ args }) {
|
|
53
|
+
var _a;
|
|
54
|
+
const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState;
|
|
55
|
+
const spanContext = instrumentationApi.agent.tracer.getSpanContext();
|
|
56
|
+
const delimiter = transactionNameState.delimiter;
|
|
57
|
+
const rootOperation = args.document.definitions.find(definitionNode => definitionNode.kind === Kind.OPERATION_DEFINITION);
|
|
58
|
+
const operationType = rootOperation.operation;
|
|
59
|
+
const document = print(args.document);
|
|
60
|
+
const operationName = args.document[options.operationNameProperty] ||
|
|
61
|
+
args.operationName ||
|
|
62
|
+
((_a = rootOperation.name) === null || _a === void 0 ? void 0 : _a.value) ||
|
|
63
|
+
AttributeName.ANONYMOUS_OPERATION;
|
|
64
|
+
let rootFields = null;
|
|
65
|
+
if (options.rootFieldsNaming) {
|
|
66
|
+
const fieldNodes = rootOperation.selectionSet.selections.filter(selectionNode => selectionNode.kind === Kind.FIELD);
|
|
67
|
+
rootFields = fieldNodes.map(fieldNode => fieldNode.name.value);
|
|
68
|
+
}
|
|
69
|
+
transactionNameState.setName(transactionNameState.prefix, transactionNameState.verb, delimiter, operationType + delimiter + operationName + (rootFields ? delimiter + rootFields.join('&') : ''));
|
|
70
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_NAME, operationName);
|
|
71
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_TYPE, operationType);
|
|
72
|
+
options.includeOperationDocument && spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_DOCUMENT, document);
|
|
73
|
+
if (options.includeExecuteVariables) {
|
|
74
|
+
const rawVariables = args.variableValues || {};
|
|
75
|
+
const executeVariablesToTrack = options.isExecuteVariablesRegex
|
|
76
|
+
? filterPropertiesByRegex(rawVariables, options.includeExecuteVariables)
|
|
77
|
+
: rawVariables;
|
|
78
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_VARIABLES, JSON.stringify(executeVariablesToTrack));
|
|
79
|
+
}
|
|
80
|
+
const operationSegment = instrumentationApi.getActiveSegment();
|
|
81
|
+
const onResolverCalled = options.trackResolvers
|
|
82
|
+
? ({ args: resolversArgs, info }) => {
|
|
83
|
+
const { returnType, path, parentType } = info;
|
|
84
|
+
const formattedPath = flattenPath(path, delimiter);
|
|
85
|
+
const currentSegment = instrumentationApi.getActiveSegment();
|
|
86
|
+
if (!currentSegment) {
|
|
87
|
+
logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath);
|
|
88
|
+
return () => { };
|
|
89
|
+
}
|
|
90
|
+
const resolverSegment = instrumentationApi.createSegment(`resolver${delimiter}${formattedPath}`, null, operationSegment);
|
|
91
|
+
if (!resolverSegment) {
|
|
92
|
+
logger.trace('Resolver segment was not created (%s).', formattedPath);
|
|
93
|
+
return () => { };
|
|
94
|
+
}
|
|
95
|
+
resolverSegment.start();
|
|
96
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath);
|
|
97
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString());
|
|
98
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString());
|
|
99
|
+
if (options.includeResolverArgs) {
|
|
100
|
+
const rawArgs = resolversArgs || {};
|
|
101
|
+
const resolverArgsToTrack = options.isResolverArgsRegex
|
|
102
|
+
? filterPropertiesByRegex(rawArgs, options.includeResolverArgs)
|
|
103
|
+
: rawArgs;
|
|
104
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack));
|
|
105
|
+
}
|
|
106
|
+
return ({ result }) => {
|
|
107
|
+
if (options.includeRawResult) {
|
|
108
|
+
resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result));
|
|
109
|
+
}
|
|
110
|
+
if (result instanceof Error) {
|
|
111
|
+
const transaction = instrumentationApi.tracer.getTransaction();
|
|
112
|
+
instrumentationApi.agent.errors.add(transaction, JSON.stringify(result));
|
|
113
|
+
}
|
|
114
|
+
resolverSegment.end();
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
: undefined;
|
|
118
|
+
return {
|
|
119
|
+
onResolverCalled,
|
|
120
|
+
onExecuteDone({ result }) {
|
|
121
|
+
if (isAsyncIterable(result)) {
|
|
122
|
+
operationSegment.end();
|
|
123
|
+
// eslint-disable-next-line no-console
|
|
124
|
+
console.warn(`Plugin "newrelic" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (result.data && options.includeRawResult) {
|
|
128
|
+
spanContext.addCustomAttribute(AttributeName.EXECUTION_RESULT, JSON.stringify(result));
|
|
129
|
+
}
|
|
130
|
+
if (result.errors && result.errors.length > 0) {
|
|
131
|
+
const agent = instrumentationApi.agent;
|
|
132
|
+
const transaction = instrumentationApi.tracer.getTransaction();
|
|
133
|
+
for (const error of result.errors) {
|
|
134
|
+
agent.errors.add(transaction, JSON.stringify(error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
operationSegment.end();
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
function flattenPath(fieldPath, delimiter = '/') {
|
|
144
|
+
const pathArray = [];
|
|
145
|
+
let thisPath = fieldPath;
|
|
146
|
+
while (thisPath) {
|
|
147
|
+
if (typeof thisPath.key !== 'number') {
|
|
148
|
+
pathArray.push(thisPath.key);
|
|
149
|
+
}
|
|
150
|
+
thisPath = thisPath.prev;
|
|
151
|
+
}
|
|
152
|
+
return pathArray.reverse().join(delimiter);
|
|
153
|
+
}
|
|
154
|
+
function filterPropertiesByRegex(initialObject, pattern) {
|
|
155
|
+
const filteredObject = {};
|
|
156
|
+
for (const property of Object.keys(initialObject)) {
|
|
157
|
+
if (pattern.test(property))
|
|
158
|
+
filteredObject[property] = initialObject[property];
|
|
159
|
+
}
|
|
160
|
+
return filteredObject;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { useNewRelic };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@envelop/newrelic",
|
|
3
|
+
"version": "1.0.3-alpha-52fc7d2.0",
|
|
4
|
+
"sideEffects": false,
|
|
5
|
+
"peerDependencies": {
|
|
6
|
+
"graphql": "^14.0.0 || ^15.0.0",
|
|
7
|
+
"newrelic": "^7 || ^8.0.0"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/dotansimha/envelop.git",
|
|
12
|
+
"directory": "packages/plugins/newrelic"
|
|
13
|
+
},
|
|
14
|
+
"author": "Santino Puleio <santinopuleio@gmail.com>",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"module": "index.mjs",
|
|
18
|
+
"typings": "index.d.ts",
|
|
19
|
+
"typescript": {
|
|
20
|
+
"definition": "index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"require": "./index.js",
|
|
25
|
+
"import": "./index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./*": {
|
|
28
|
+
"require": "./*.js",
|
|
29
|
+
"import": "./*.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|