mongo.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import { _optionalChain } from '@sentry/utils';
  2. import { loadModule, logger, fill, isThenable } from '@sentry/utils';
  3. import { DEBUG_BUILD } from '../../common/debug-build.js';
  4. import { shouldDisableAutoInstrumentation } from './utils/node-utils.js';
  5. // This allows us to use the same array for both defaults options and the type itself.
  6. // (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... )
  7. // and not just a string[])
  8. const OPERATIONS = [
  9. 'aggregate', // aggregate(pipeline, options, callback)
  10. 'bulkWrite', // bulkWrite(operations, options, callback)
  11. 'countDocuments', // countDocuments(query, options, callback)
  12. 'createIndex', // createIndex(fieldOrSpec, options, callback)
  13. 'createIndexes', // createIndexes(indexSpecs, options, callback)
  14. 'deleteMany', // deleteMany(filter, options, callback)
  15. 'deleteOne', // deleteOne(filter, options, callback)
  16. 'distinct', // distinct(key, query, options, callback)
  17. 'drop', // drop(options, callback)
  18. 'dropIndex', // dropIndex(indexName, options, callback)
  19. 'dropIndexes', // dropIndexes(options, callback)
  20. 'estimatedDocumentCount', // estimatedDocumentCount(options, callback)
  21. 'find', // find(query, options, callback)
  22. 'findOne', // findOne(query, options, callback)
  23. 'findOneAndDelete', // findOneAndDelete(filter, options, callback)
  24. 'findOneAndReplace', // findOneAndReplace(filter, replacement, options, callback)
  25. 'findOneAndUpdate', // findOneAndUpdate(filter, update, options, callback)
  26. 'indexes', // indexes(options, callback)
  27. 'indexExists', // indexExists(indexes, options, callback)
  28. 'indexInformation', // indexInformation(options, callback)
  29. 'initializeOrderedBulkOp', // initializeOrderedBulkOp(options, callback)
  30. 'insertMany', // insertMany(docs, options, callback)
  31. 'insertOne', // insertOne(doc, options, callback)
  32. 'isCapped', // isCapped(options, callback)
  33. 'mapReduce', // mapReduce(map, reduce, options, callback)
  34. 'options', // options(options, callback)
  35. 'parallelCollectionScan', // parallelCollectionScan(options, callback)
  36. 'rename', // rename(newName, options, callback)
  37. 'replaceOne', // replaceOne(filter, doc, options, callback)
  38. 'stats', // stats(options, callback)
  39. 'updateMany', // updateMany(filter, update, options, callback)
  40. 'updateOne', // updateOne(filter, update, options, callback)
  41. ] ;
  42. // All of the operations above take `options` and `callback` as their final parameters, but some of them
  43. // take additional parameters as well. For those operations, this is a map of
  44. // { <operation name>: [<names of additional parameters>] }, as a way to know what to call the operation's
  45. // positional arguments when we add them to the span's `data` object later
  46. const OPERATION_SIGNATURES
  47. = {
  48. // aggregate intentionally not included because `pipeline` arguments are too complex to serialize well
  49. // see https://github.com/getsentry/sentry-javascript/pull/3102
  50. bulkWrite: ['operations'],
  51. countDocuments: ['query'],
  52. createIndex: ['fieldOrSpec'],
  53. createIndexes: ['indexSpecs'],
  54. deleteMany: ['filter'],
  55. deleteOne: ['filter'],
  56. distinct: ['key', 'query'],
  57. dropIndex: ['indexName'],
  58. find: ['query'],
  59. findOne: ['query'],
  60. findOneAndDelete: ['filter'],
  61. findOneAndReplace: ['filter', 'replacement'],
  62. findOneAndUpdate: ['filter', 'update'],
  63. indexExists: ['indexes'],
  64. insertMany: ['docs'],
  65. insertOne: ['doc'],
  66. mapReduce: ['map', 'reduce'],
  67. rename: ['newName'],
  68. replaceOne: ['filter', 'doc'],
  69. updateMany: ['filter', 'update'],
  70. updateOne: ['filter', 'update'],
  71. };
  72. function isCursor(maybeCursor) {
  73. return maybeCursor && typeof maybeCursor === 'object' && maybeCursor.once && typeof maybeCursor.once === 'function';
  74. }
  75. /** Tracing integration for mongo package */
  76. class Mongo {
  77. /**
  78. * @inheritDoc
  79. */
  80. static __initStatic() {this.id = 'Mongo';}
  81. /**
  82. * @inheritDoc
  83. */
  84. /**
  85. * @inheritDoc
  86. */
  87. constructor(options = {}) {
  88. this.name = Mongo.id;
  89. this._operations = Array.isArray(options.operations) ? options.operations : (OPERATIONS );
  90. this._describeOperations = 'describeOperations' in options ? options.describeOperations : true;
  91. this._useMongoose = !!options.useMongoose;
  92. }
  93. /** @inheritdoc */
  94. loadDependency() {
  95. const moduleName = this._useMongoose ? 'mongoose' : 'mongodb';
  96. return (this._module = this._module || loadModule(moduleName));
  97. }
  98. /**
  99. * @inheritDoc
  100. */
  101. setupOnce(_, getCurrentHub) {
  102. if (shouldDisableAutoInstrumentation(getCurrentHub)) {
  103. DEBUG_BUILD && logger.log('Mongo Integration is skipped because of instrumenter configuration.');
  104. return;
  105. }
  106. const pkg = this.loadDependency();
  107. if (!pkg) {
  108. const moduleName = this._useMongoose ? 'mongoose' : 'mongodb';
  109. DEBUG_BUILD && logger.error(`Mongo Integration was unable to require \`${moduleName}\` package.`);
  110. return;
  111. }
  112. this._instrumentOperations(pkg.Collection, this._operations, getCurrentHub);
  113. }
  114. /**
  115. * Patches original collection methods
  116. */
  117. _instrumentOperations(collection, operations, getCurrentHub) {
  118. operations.forEach((operation) => this._patchOperation(collection, operation, getCurrentHub));
  119. }
  120. /**
  121. * Patches original collection to utilize our tracing functionality
  122. */
  123. _patchOperation(collection, operation, getCurrentHub) {
  124. if (!(operation in collection.prototype)) return;
  125. const getSpanContext = this._getSpanContextFromOperationArguments.bind(this);
  126. fill(collection.prototype, operation, function (orig) {
  127. return function ( ...args) {
  128. const lastArg = args[args.length - 1];
  129. // eslint-disable-next-line deprecation/deprecation
  130. const hub = getCurrentHub();
  131. // eslint-disable-next-line deprecation/deprecation
  132. const scope = hub.getScope();
  133. // eslint-disable-next-line deprecation/deprecation
  134. const client = hub.getClient();
  135. // eslint-disable-next-line deprecation/deprecation
  136. const parentSpan = scope.getSpan();
  137. const sendDefaultPii = _optionalChain([client, 'optionalAccess', _2 => _2.getOptions, 'call', _3 => _3(), 'access', _4 => _4.sendDefaultPii]);
  138. // Check if the operation was passed a callback. (mapReduce requires a different check, as
  139. // its (non-callback) arguments can also be functions.)
  140. if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) {
  141. // eslint-disable-next-line deprecation/deprecation
  142. const span = _optionalChain([parentSpan, 'optionalAccess', _5 => _5.startChild, 'call', _6 => _6(getSpanContext(this, operation, args, sendDefaultPii))]);
  143. const maybePromiseOrCursor = orig.call(this, ...args);
  144. if (isThenable(maybePromiseOrCursor)) {
  145. return maybePromiseOrCursor.then((res) => {
  146. _optionalChain([span, 'optionalAccess', _7 => _7.end, 'call', _8 => _8()]);
  147. return res;
  148. });
  149. }
  150. // If the operation returns a Cursor
  151. // we need to attach a listener to it to finish the span when the cursor is closed.
  152. else if (isCursor(maybePromiseOrCursor)) {
  153. const cursor = maybePromiseOrCursor ;
  154. try {
  155. cursor.once('close', () => {
  156. _optionalChain([span, 'optionalAccess', _9 => _9.end, 'call', _10 => _10()]);
  157. });
  158. } catch (e) {
  159. // If the cursor is already closed, `once` will throw an error. In that case, we can
  160. // finish the span immediately.
  161. _optionalChain([span, 'optionalAccess', _11 => _11.end, 'call', _12 => _12()]);
  162. }
  163. return cursor;
  164. } else {
  165. _optionalChain([span, 'optionalAccess', _13 => _13.end, 'call', _14 => _14()]);
  166. return maybePromiseOrCursor;
  167. }
  168. }
  169. // eslint-disable-next-line deprecation/deprecation
  170. const span = _optionalChain([parentSpan, 'optionalAccess', _15 => _15.startChild, 'call', _16 => _16(getSpanContext(this, operation, args.slice(0, -1)))]);
  171. return orig.call(this, ...args.slice(0, -1), function (err, result) {
  172. _optionalChain([span, 'optionalAccess', _17 => _17.end, 'call', _18 => _18()]);
  173. lastArg(err, result);
  174. });
  175. };
  176. });
  177. }
  178. /**
  179. * Form a SpanContext based on the user input to a given operation.
  180. */
  181. _getSpanContextFromOperationArguments(
  182. collection,
  183. operation,
  184. args,
  185. sendDefaultPii = false,
  186. ) {
  187. const data = {
  188. 'db.system': 'mongodb',
  189. 'db.name': collection.dbName,
  190. 'db.operation': operation,
  191. 'db.mongodb.collection': collection.collectionName,
  192. };
  193. const spanContext = {
  194. op: 'db',
  195. // TODO v8: Use `${collection.collectionName}.${operation}`
  196. origin: 'auto.db.mongo',
  197. description: operation,
  198. data,
  199. };
  200. // If the operation takes no arguments besides `options` and `callback`, or if argument
  201. // collection is disabled for this operation, just return early.
  202. const signature = OPERATION_SIGNATURES[operation];
  203. const shouldDescribe = Array.isArray(this._describeOperations)
  204. ? this._describeOperations.includes(operation)
  205. : this._describeOperations;
  206. if (!signature || !shouldDescribe || !sendDefaultPii) {
  207. return spanContext;
  208. }
  209. try {
  210. // Special case for `mapReduce`, as the only one accepting functions as arguments.
  211. if (operation === 'mapReduce') {
  212. const [map, reduce] = args ;
  213. data[signature[0]] = typeof map === 'string' ? map : map.name || '<anonymous>';
  214. data[signature[1]] = typeof reduce === 'string' ? reduce : reduce.name || '<anonymous>';
  215. } else {
  216. for (let i = 0; i < signature.length; i++) {
  217. data[`db.mongodb.${signature[i]}`] = JSON.stringify(args[i]);
  218. }
  219. }
  220. } catch (_oO) {
  221. // no-empty
  222. }
  223. return spanContext;
  224. }
  225. }Mongo.__initStatic();
  226. export { Mongo };
  227. //# sourceMappingURL=mongo.js.map