import { GraphQLError, } from 'graphql'; import { Trace, google } from '@apollo/usage-reporting-protobuf'; import { UnreachableCaseError } from '../utils/UnreachableCaseError.js'; function internalError(message) { return new Error(`[internal apollo-server error] ${message}`); } export class TraceTreeBuilder { constructor(options) { this.rootNode = new Trace.Node(); this.trace = new Trace({ root: this.rootNode, fieldExecutionWeight: 1, }); this.stopped = false; this.nodes = new Map([ [responsePathAsString(), this.rootNode], ]); const { sendErrors, maskedBy } = options; if (!sendErrors || 'masked' in sendErrors) { this.transformError = () => new GraphQLError('', { extensions: { maskedBy }, }); } else if ('transform' in sendErrors) { this.transformError = sendErrors.transform; } else if ('unmodified' in sendErrors) { this.transformError = null; } else { throw new UnreachableCaseError(sendErrors); } } startTiming() { if (this.startHrTime) { throw internalError('startTiming called twice!'); } if (this.stopped) { throw internalError('startTiming called after stopTiming!'); } this.trace.startTime = dateToProtoTimestamp(new Date()); this.startHrTime = process.hrtime(); } stopTiming() { if (!this.startHrTime) { throw internalError('stopTiming called before startTiming!'); } if (this.stopped) { throw internalError('stopTiming called twice!'); } this.trace.durationNs = durationHrTimeToNanos(process.hrtime(this.startHrTime)); this.trace.endTime = dateToProtoTimestamp(new Date()); this.stopped = true; } willResolveField(info) { if (!this.startHrTime) { throw internalError('willResolveField called before startTiming!'); } if (this.stopped) { return () => { }; } const path = info.path; const node = this.newNode(path); node.type = info.returnType.toString(); node.parentType = info.parentType.toString(); node.startTime = durationHrTimeToNanos(process.hrtime(this.startHrTime)); if (typeof path.key === 'string' && path.key !== info.fieldName) { node.originalFieldName = info.fieldName; } return () => { node.endTime = durationHrTimeToNanos(process.hrtime(this.startHrTime)); }; } didEncounterErrors(errors) { errors.forEach((err) => { if (err.extensions?.serviceName) { return; } const errorForReporting = this.transformAndNormalizeError(err); if (errorForReporting === null) { return; } this.addProtobufError(errorForReporting.path, errorToProtobufError(errorForReporting)); }); } addProtobufError(path, error) { if (!this.startHrTime) { throw internalError('addProtobufError called before startTiming!'); } if (this.stopped) { throw internalError('addProtobufError called after stopTiming!'); } let node = this.rootNode; if (Array.isArray(path)) { const specificNode = this.nodes.get(path.join('.')); if (specificNode) { node = specificNode; } else { const responsePath = responsePathFromArray(path, this.rootNode); if (!responsePath) { throw internalError('addProtobufError called with invalid path!'); } node = this.newNode(responsePath); } } node.error.push(error); } newNode(path) { const node = new Trace.Node(); const id = path.key; if (typeof id === 'number') { node.index = id; } else { node.responseName = id; } this.nodes.set(responsePathAsString(path), node); const parentNode = this.ensureParentNode(path); parentNode.child.push(node); return node; } ensureParentNode(path) { const parentPath = responsePathAsString(path.prev); const parentNode = this.nodes.get(parentPath); if (parentNode) { return parentNode; } return this.newNode(path.prev); } transformAndNormalizeError(err) { if (this.transformError) { const clonedError = Object.assign(Object.create(Object.getPrototypeOf(err)), err); const rewrittenError = this.transformError(clonedError); if (rewrittenError === null) { return null; } if (!(rewrittenError instanceof GraphQLError)) { return err; } return new GraphQLError(rewrittenError.message, { nodes: err.nodes, source: err.source, positions: err.positions, path: err.path, originalError: err.originalError, extensions: rewrittenError.extensions || err.extensions, }); } return err; } } function durationHrTimeToNanos(hrtime) { return hrtime[0] * 1e9 + hrtime[1]; } function responsePathAsString(p) { if (p === undefined) { return ''; } let res = String(p.key); while ((p = p.prev) !== undefined) { res = `${p.key}.${res}`; } return res; } function responsePathFromArray(path, node) { let responsePath; let nodePtr = node; for (const key of path) { nodePtr = nodePtr?.child?.find((child) => child.responseName === key); responsePath = { key, prev: responsePath, typename: nodePtr?.type ?? undefined, }; } return responsePath; } function errorToProtobufError(error) { return new Trace.Error({ message: error.message, location: (error.locations || []).map(({ line, column }) => new Trace.Location({ line, column })), json: JSON.stringify(error), }); } export function dateToProtoTimestamp(date) { const totalMillis = +date; const millis = totalMillis % 1000; return new google.protobuf.Timestamp({ seconds: (totalMillis - millis) / 1000, nanos: millis * 1e6, }); } //# sourceMappingURL=traceTreeBuilder.js.map