traceTreeBuilder.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { GraphQLError, } from 'graphql';
  2. import { Trace, google } from '@apollo/usage-reporting-protobuf';
  3. import { UnreachableCaseError } from '../utils/UnreachableCaseError.js';
  4. function internalError(message) {
  5. return new Error(`[internal apollo-server error] ${message}`);
  6. }
  7. export class TraceTreeBuilder {
  8. constructor(options) {
  9. this.rootNode = new Trace.Node();
  10. this.trace = new Trace({
  11. root: this.rootNode,
  12. fieldExecutionWeight: 1,
  13. });
  14. this.stopped = false;
  15. this.nodes = new Map([
  16. [responsePathAsString(), this.rootNode],
  17. ]);
  18. const { sendErrors, maskedBy } = options;
  19. if (!sendErrors || 'masked' in sendErrors) {
  20. this.transformError = () => new GraphQLError('<masked>', {
  21. extensions: { maskedBy },
  22. });
  23. }
  24. else if ('transform' in sendErrors) {
  25. this.transformError = sendErrors.transform;
  26. }
  27. else if ('unmodified' in sendErrors) {
  28. this.transformError = null;
  29. }
  30. else {
  31. throw new UnreachableCaseError(sendErrors);
  32. }
  33. }
  34. startTiming() {
  35. if (this.startHrTime) {
  36. throw internalError('startTiming called twice!');
  37. }
  38. if (this.stopped) {
  39. throw internalError('startTiming called after stopTiming!');
  40. }
  41. this.trace.startTime = dateToProtoTimestamp(new Date());
  42. this.startHrTime = process.hrtime();
  43. }
  44. stopTiming() {
  45. if (!this.startHrTime) {
  46. throw internalError('stopTiming called before startTiming!');
  47. }
  48. if (this.stopped) {
  49. throw internalError('stopTiming called twice!');
  50. }
  51. this.trace.durationNs = durationHrTimeToNanos(process.hrtime(this.startHrTime));
  52. this.trace.endTime = dateToProtoTimestamp(new Date());
  53. this.stopped = true;
  54. }
  55. willResolveField(info) {
  56. if (!this.startHrTime) {
  57. throw internalError('willResolveField called before startTiming!');
  58. }
  59. if (this.stopped) {
  60. return () => { };
  61. }
  62. const path = info.path;
  63. const node = this.newNode(path);
  64. node.type = info.returnType.toString();
  65. node.parentType = info.parentType.toString();
  66. node.startTime = durationHrTimeToNanos(process.hrtime(this.startHrTime));
  67. if (typeof path.key === 'string' && path.key !== info.fieldName) {
  68. node.originalFieldName = info.fieldName;
  69. }
  70. return () => {
  71. node.endTime = durationHrTimeToNanos(process.hrtime(this.startHrTime));
  72. };
  73. }
  74. didEncounterErrors(errors) {
  75. errors.forEach((err) => {
  76. if (err.extensions?.serviceName) {
  77. return;
  78. }
  79. const errorForReporting = this.transformAndNormalizeError(err);
  80. if (errorForReporting === null) {
  81. return;
  82. }
  83. this.addProtobufError(errorForReporting.path, errorToProtobufError(errorForReporting));
  84. });
  85. }
  86. addProtobufError(path, error) {
  87. if (!this.startHrTime) {
  88. throw internalError('addProtobufError called before startTiming!');
  89. }
  90. if (this.stopped) {
  91. throw internalError('addProtobufError called after stopTiming!');
  92. }
  93. let node = this.rootNode;
  94. if (Array.isArray(path)) {
  95. const specificNode = this.nodes.get(path.join('.'));
  96. if (specificNode) {
  97. node = specificNode;
  98. }
  99. else {
  100. const responsePath = responsePathFromArray(path, this.rootNode);
  101. if (!responsePath) {
  102. throw internalError('addProtobufError called with invalid path!');
  103. }
  104. node = this.newNode(responsePath);
  105. }
  106. }
  107. node.error.push(error);
  108. }
  109. newNode(path) {
  110. const node = new Trace.Node();
  111. const id = path.key;
  112. if (typeof id === 'number') {
  113. node.index = id;
  114. }
  115. else {
  116. node.responseName = id;
  117. }
  118. this.nodes.set(responsePathAsString(path), node);
  119. const parentNode = this.ensureParentNode(path);
  120. parentNode.child.push(node);
  121. return node;
  122. }
  123. ensureParentNode(path) {
  124. const parentPath = responsePathAsString(path.prev);
  125. const parentNode = this.nodes.get(parentPath);
  126. if (parentNode) {
  127. return parentNode;
  128. }
  129. return this.newNode(path.prev);
  130. }
  131. transformAndNormalizeError(err) {
  132. if (this.transformError) {
  133. const clonedError = Object.assign(Object.create(Object.getPrototypeOf(err)), err);
  134. const rewrittenError = this.transformError(clonedError);
  135. if (rewrittenError === null) {
  136. return null;
  137. }
  138. if (!(rewrittenError instanceof GraphQLError)) {
  139. return err;
  140. }
  141. return new GraphQLError(rewrittenError.message, {
  142. nodes: err.nodes,
  143. source: err.source,
  144. positions: err.positions,
  145. path: err.path,
  146. originalError: err.originalError,
  147. extensions: rewrittenError.extensions || err.extensions,
  148. });
  149. }
  150. return err;
  151. }
  152. }
  153. function durationHrTimeToNanos(hrtime) {
  154. return hrtime[0] * 1e9 + hrtime[1];
  155. }
  156. function responsePathAsString(p) {
  157. if (p === undefined) {
  158. return '';
  159. }
  160. let res = String(p.key);
  161. while ((p = p.prev) !== undefined) {
  162. res = `${p.key}.${res}`;
  163. }
  164. return res;
  165. }
  166. function responsePathFromArray(path, node) {
  167. let responsePath;
  168. let nodePtr = node;
  169. for (const key of path) {
  170. nodePtr = nodePtr?.child?.find((child) => child.responseName === key);
  171. responsePath = {
  172. key,
  173. prev: responsePath,
  174. typename: nodePtr?.type ?? undefined,
  175. };
  176. }
  177. return responsePath;
  178. }
  179. function errorToProtobufError(error) {
  180. return new Trace.Error({
  181. message: error.message,
  182. location: (error.locations || []).map(({ line, column }) => new Trace.Location({ line, column })),
  183. json: JSON.stringify(error),
  184. });
  185. }
  186. export function dateToProtoTimestamp(date) {
  187. const totalMillis = +date;
  188. const millis = totalMillis % 1000;
  189. return new google.protobuf.Timestamp({
  190. seconds: (totalMillis - millis) / 1000,
  191. nanos: millis * 1e6,
  192. });
  193. }
  194. //# sourceMappingURL=traceTreeBuilder.js.map