Skip to content

Commit 5808561

Browse files
authored
fix(newrelic): handle invalid newrelic env vars correctly (#1606)
1 parent fe2b6b0 commit 5808561

File tree

6 files changed

+422
-144
lines changed

6 files changed

+422
-144
lines changed

.changeset/dirty-poets-exercise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@envelop/newrelic': patch
3+
---
4+
5+
Fix `Cannot read properties of null (reading 'namestate')` error when NewRelic env vars are invalid

packages/plugins/newrelic/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,12 @@
5656
"tslib": "^2.5.0"
5757
},
5858
"devDependencies": {
59-
"@envelop/core": "^4.0.0",
6059
"@types/newrelic": "9.14.0",
60+
"@envelop/core": "^4.0.0",
6161
"graphql": "16.6.0",
62-
"newrelic": "8.17.1",
63-
"typescript": "5.1.3"
62+
"newrelic": "9.8.1",
63+
"typescript": "4.8.4",
64+
"@newrelic/test-utilities": "6.5.5"
6465
},
6566
"publishConfig": {
6667
"directory": "dist",

packages/plugins/newrelic/src/index.ts

Lines changed: 75 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
1-
import {
2-
ExecutionResult,
3-
FieldNode,
4-
GraphQLError,
5-
Kind,
6-
OperationDefinitionNode,
7-
print,
8-
} from 'graphql';
1+
import { ExecutionResult, FieldNode, getOperationAST, GraphQLError, Kind, print } from 'graphql';
2+
import newRelic from 'newrelic';
93
import { DefaultContext, getDocumentString, isAsyncIterable, Path, Plugin } from '@envelop/core';
104
import { useOnResolve } from '@envelop/on-resolve';
115

12-
enum AttributeName {
6+
export enum AttributeName {
137
COMPONENT_NAME = 'Envelop_NewRelic_Plugin',
148
ANONYMOUS_OPERATION = '<anonymous>',
159
EXECUTION_RESULT = 'graphql.execute.result',
@@ -40,6 +34,8 @@ export type UseNewRelicOptions = {
4034
* By default, this plugin skips all `Error` errors and does not report them to NewRelic.
4135
*/
4236
skipError?: (error: GraphQLError) => boolean;
37+
38+
shim?: any;
4339
};
4440

4541
interface InternalOptions extends UseNewRelicOptions {
@@ -64,36 +60,38 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
6460
};
6561
options.isExecuteVariablesRegex = options.includeExecuteVariables instanceof RegExp;
6662
options.isResolverArgsRegex = options.includeResolverArgs instanceof RegExp;
67-
const instrumentationApi$ = import('newrelic')
68-
.then(m => m.default || m)
69-
.then(({ shim }) => {
70-
if (!shim?.agent) {
71-
throw new Error(
72-
'Agent unavailable. Please check your New Relic Agent configuration and ensure New Relic is enabled.',
73-
);
74-
}
75-
shim.agent.metrics
76-
.getOrCreateMetric(`Supportability/ExternalModules/${AttributeName.COMPONENT_NAME}`)
77-
.incrementCallCount();
78-
return shim;
79-
});
63+
const instrumentationApi = rawOptions?.shim || newRelic?.shim;
64+
if (!instrumentationApi?.agent) {
65+
// eslint-disable-next-line no-console
66+
console.warn(
67+
'Agent unavailable. Please check your New Relic Agent configuration and ensure New Relic is enabled.',
68+
);
69+
return {};
70+
}
71+
instrumentationApi.agent.metrics
72+
.getOrCreateMetric(`Supportability/ExternalModules/${AttributeName.COMPONENT_NAME}`)
73+
.incrementCallCount();
8074

81-
const logger$ = instrumentationApi$.then(({ logger }) => {
82-
const childLogger = logger.child({ component: AttributeName.COMPONENT_NAME });
83-
childLogger.info(`${AttributeName.COMPONENT_NAME} registered`);
84-
return childLogger;
85-
});
75+
const logger = instrumentationApi.logger.child({ component: AttributeName.COMPONENT_NAME });
76+
logger.info(`${AttributeName.COMPONENT_NAME} registered`);
8677

8778
return {
8879
onPluginInit({ addPlugin }) {
8980
if (options.trackResolvers) {
9081
addPlugin(
91-
useOnResolve(async ({ args: resolversArgs, info }) => {
92-
const instrumentationApi = await instrumentationApi$;
93-
const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState;
82+
useOnResolve(({ args: resolversArgs, info }) => {
83+
const transaction = instrumentationApi.agent.tracer.getTransaction();
84+
if (!transaction) {
85+
logger.trace('No transaction found. Not recording resolver.');
86+
return () => { };
87+
}
88+
const transactionNameState = transaction.nameState;
89+
if (!transactionNameState) {
90+
logger.trace('No transaction name state found. Not recording resolver.');
91+
return () => { };
92+
}
9493
const delimiter = transactionNameState.delimiter;
9594

96-
const logger = await logger$;
9795
const { returnType, path, parentType } = info;
9896
const formattedPath = flattenPath(path, delimiter);
9997
const currentSegment = instrumentationApi.getActiveSegment();
@@ -102,7 +100,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
102100
'No active segment found at resolver call. Not recording resolver (%s).',
103101
formattedPath,
104102
);
105-
return () => {};
103+
return () => { };
106104
}
107105

108106
const resolverSegment = instrumentationApi.createSegment(
@@ -112,7 +110,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
112110
);
113111
if (!resolverSegment) {
114112
logger.trace('Resolver segment was not created (%s).', formattedPath);
115-
return () => {};
113+
return () => { };
116114
}
117115
resolverSegment.start();
118116
resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath);
@@ -138,53 +136,62 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
138136
);
139137
}
140138
},
141-
async onExecute({ args }) {
142-
const instrumentationApi = await instrumentationApi$;
143-
const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState;
144-
const spanContext = instrumentationApi.agent.tracer.getSpanContext();
145-
const delimiter = transactionNameState.delimiter;
146-
const rootOperation = args.document.definitions.find(
147-
// @ts-expect-error TODO: not sure how we will make it dev friendly
148-
definitionNode => definitionNode.kind === Kind.OPERATION_DEFINITION,
149-
) as OperationDefinitionNode;
139+
onExecute({ args }) {
140+
const rootOperation = getOperationAST(args.document, args.operationName);
141+
if (!rootOperation) {
142+
logger.trace('No root operation found. Not recording transaction.');
143+
return;
144+
}
150145
const operationType = rootOperation.operation;
151-
const document = getDocumentString(args.document, print);
152146
const operationName =
153147
options.extractOperationName?.(args.contextValue) ||
154148
args.operationName ||
155149
rootOperation.name?.value ||
156150
AttributeName.ANONYMOUS_OPERATION;
157-
let rootFields: string[] | null = null;
158151

159-
if (options.rootFieldsNaming) {
160-
const fieldNodes = rootOperation.selectionSet.selections.filter(
161-
selectionNode => selectionNode.kind === Kind.FIELD,
162-
) as FieldNode[];
163-
rootFields = fieldNodes.map(fieldNode => fieldNode.name.value);
164-
}
152+
const transaction = instrumentationApi.agent.tracer.getTransaction();
153+
const transactionNameState = transaction?.nameState;
154+
if (transactionNameState) {
155+
const delimiter = transactionNameState.delimiter || '/';
156+
let rootFields: string[] | null = null;
157+
158+
if (options.rootFieldsNaming) {
159+
const fieldNodes = rootOperation.selectionSet.selections.filter(
160+
selectionNode => selectionNode.kind === Kind.FIELD,
161+
) as FieldNode[];
162+
rootFields = fieldNodes.map(fieldNode => fieldNode.name.value);
163+
}
165164

166-
transactionNameState.setName(
167-
transactionNameState.prefix,
168-
transactionNameState.verb,
169-
delimiter,
170-
operationType +
165+
const operationType = rootOperation.operation;
166+
167+
transactionNameState.setName(
168+
transactionNameState.prefix,
169+
transactionNameState.verb,
170+
delimiter,
171+
operationType +
171172
delimiter +
172173
operationName +
173174
(rootFields ? delimiter + rootFields.join('&') : ''),
174-
);
175+
);
176+
}
177+
178+
const spanContext = instrumentationApi.agent.tracer.getSpanContext();
175179

176-
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_NAME, operationName);
177-
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_TYPE, operationType);
180+
spanContext?.addCustomAttribute(AttributeName.EXECUTION_OPERATION_NAME, operationName);
181+
spanContext?.addCustomAttribute(AttributeName.EXECUTION_OPERATION_TYPE, operationType);
178182
options.includeOperationDocument &&
179-
spanContext.addCustomAttribute(AttributeName.EXECUTION_OPERATION_DOCUMENT, document);
183+
spanContext?.addCustomAttribute(
184+
AttributeName.EXECUTION_OPERATION_DOCUMENT,
185+
getDocumentString(args.document, print),
186+
);
180187

181188
if (options.includeExecuteVariables) {
182189
const rawVariables = args.variableValues || {};
183190
const executeVariablesToTrack = options.isExecuteVariablesRegex
184191
? filterPropertiesByRegex(rawVariables, options.includeExecuteVariables as RegExp)
185192
: rawVariables;
186193

187-
spanContext.addCustomAttribute(
194+
spanContext?.addCustomAttribute(
188195
AttributeName.EXECUTION_VARIABLES,
189196
JSON.stringify(executeVariablesToTrack),
190197
);
@@ -196,7 +203,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
196203
onExecuteDone({ result }) {
197204
const sendResult = (singularResult: ExecutionResult) => {
198205
if (singularResult.data && options.includeRawResult) {
199-
spanContext.addCustomAttribute(
206+
spanContext?.addCustomAttribute(
200207
AttributeName.EXECUTION_RESULT,
201208
JSON.stringify(singularResult),
202209
);
@@ -206,9 +213,11 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
206213
const agent = instrumentationApi.agent;
207214
const transaction = instrumentationApi.tracer.getTransaction();
208215

209-
for (const error of singularResult.errors) {
210-
if (options.skipError?.(error)) continue;
211-
agent.errors.add(transaction, JSON.stringify(error));
216+
if (transaction) {
217+
for (const error of singularResult.errors) {
218+
if (options.skipError?.(error)) continue;
219+
agent.errors.add(transaction, JSON.stringify(error));
220+
}
212221
}
213222
}
214223
};
@@ -218,12 +227,12 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => {
218227
sendResult(singularResult);
219228
},
220229
onEnd: () => {
221-
operationSegment.end();
230+
operationSegment?.end();
222231
},
223232
};
224233
}
225234
sendResult(result);
226-
operationSegment.end();
235+
operationSegment?.end();
227236
return {};
228237
},
229238
};

0 commit comments

Comments
 (0)