index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /* eslint-env node */
  2. const process = require('process'); // eslint-disable-line import/no-nodejs-modules
  3. // TODO: Make this configurable
  4. const JsDomEnvironment = require('@visual-snapshot/jest-environment');
  5. const Sentry = require('@sentry/node');
  6. require('@sentry/tracing');
  7. function isNotTransaction(span) {
  8. return span.op !== 'jest test';
  9. }
  10. class SentryEnvironment extends JsDomEnvironment {
  11. constructor(config, context) {
  12. super(config, context);
  13. if (!config.testEnvironmentOptions || !config.testEnvironmentOptions.SENTRY_DSN) {
  14. return;
  15. }
  16. this.Sentry = Sentry;
  17. this.Sentry.init({
  18. dsn: config.testEnvironmentOptions.SENTRY_DSN,
  19. tracesSampleRate: 1.0,
  20. environment: !!process.env.CI ? 'ci' : 'local',
  21. });
  22. this.testPath = context.testPath.replace(process.cwd(), '');
  23. this.runDescribe = new Map();
  24. this.testContainers = new Map();
  25. this.tests = new Map();
  26. this.hooks = new Map();
  27. }
  28. async setup() {
  29. if (!this.Sentry) {
  30. await super.setup();
  31. return;
  32. }
  33. this.transaction = Sentry.startTransaction({
  34. op: 'jest test suite',
  35. description: this.testPath,
  36. name: this.testPath,
  37. tags: {
  38. branch: process.env.GITHUB_REF,
  39. commit: process.env.GITHUB_SHA,
  40. },
  41. });
  42. Sentry.configureScope(scope => scope.setSpan(this.transaction));
  43. const span = this.transaction.startChild({
  44. op: 'setup',
  45. description: this.testPath,
  46. });
  47. await super.setup();
  48. span.finish();
  49. }
  50. async teardown() {
  51. if (!this.Sentry) {
  52. await super.teardown();
  53. return;
  54. }
  55. const span = this.transaction.startChild({
  56. op: 'teardown',
  57. description: this.testPath,
  58. });
  59. await super.teardown();
  60. span.finish();
  61. if (this.transaction) {
  62. this.transaction.finish();
  63. }
  64. this.runDescribe = null;
  65. this.testContainers = null;
  66. this.tests = null;
  67. this.hooks = null;
  68. this.hub = null;
  69. this.Sentry = null;
  70. }
  71. runScript(script) {
  72. // We are intentionally not instrumenting this as it will produce hundreds of spans.
  73. return super.runScript(script);
  74. }
  75. getName(parent) {
  76. if (!parent) {
  77. return '';
  78. }
  79. // Ignore these for now as it adds a level of nesting and I'm not quite sure where it's even coming from
  80. if (parent.name === 'ROOT_DESCRIBE_BLOCK') {
  81. return '';
  82. }
  83. const parentName = this.getName(parent.parent);
  84. return `${parentName ? `${parentName} >` : ''} ${parent.name}`;
  85. }
  86. getData({name, ...event}) {
  87. switch (name) {
  88. case 'run_describe_start':
  89. case 'run_describe_finish':
  90. return {
  91. op: 'describe',
  92. obj: event.describeBlock,
  93. parentObj: event.describeBlock.parent,
  94. dataStore: this.runDescribe,
  95. parentStore: this.runDescribe,
  96. };
  97. case 'test_start':
  98. case 'test_done':
  99. return {
  100. op: 'test',
  101. obj: event.test,
  102. parentObj: event.test.parent,
  103. dataStore: this.testContainers,
  104. parentStore: this.runDescribe,
  105. beforeFinish: span => {
  106. span.setStatus(!event.test.errors.length ? 'ok' : 'unknown_error');
  107. return span;
  108. },
  109. };
  110. case 'test_fn_start':
  111. case 'test_fn_success':
  112. case 'test_fn_failure':
  113. return {
  114. op: 'test-fn',
  115. obj: event.test,
  116. parentObj: event.test,
  117. dataStore: this.tests,
  118. parentStore: this.testContainers,
  119. beforeFinish: span => {
  120. span.setStatus(!event.test.errors.length ? 'ok' : 'unknown_error');
  121. return span;
  122. },
  123. };
  124. case 'hook_start':
  125. return {
  126. obj: event.hook.parent,
  127. op: event.hook.type,
  128. dataStore: this.hooks,
  129. };
  130. case 'hook_success':
  131. case 'hook_failure':
  132. return {
  133. obj: event.hook.parent,
  134. parentObj: event.test && event.test.parent,
  135. dataStore: this.hooks,
  136. parentStore: this.testContainers,
  137. beforeFinish: span => {
  138. const parent = this.testContainers.get(this.getName(event.test));
  139. if (parent && !Array.isArray(parent)) {
  140. return parent.child(span);
  141. } else if (Array.isArray(parent)) {
  142. return parent.find(isNotTransaction).child(span);
  143. }
  144. return span;
  145. },
  146. };
  147. case 'start_describe_definition':
  148. case 'finish_describe_definition':
  149. case 'add_test':
  150. case 'add_hook':
  151. case 'run_start':
  152. case 'run_finish':
  153. case 'test_todo':
  154. case 'setup':
  155. case 'teardown':
  156. return null;
  157. default:
  158. return null;
  159. }
  160. }
  161. handleTestEvent(event) {
  162. if (!this.Sentry) {
  163. return;
  164. }
  165. const data = this.getData(event);
  166. const {name} = event;
  167. if (!data) {
  168. return;
  169. }
  170. const {obj, parentObj, dataStore, parentStore, op, description, beforeFinish} = data;
  171. const testName = this.getName(obj);
  172. if (name.includes('start')) {
  173. // Make this an option maybe
  174. if (!testName) {
  175. return;
  176. }
  177. const spans = [];
  178. const parentName = parentObj && this.getName(parentObj);
  179. const spanProps = {op, description: description || testName};
  180. const span =
  181. parentObj && parentStore.has(parentName)
  182. ? Array.isArray(parentStore.get(parentName))
  183. ? parentStore
  184. .get(parentName)
  185. .map(s =>
  186. typeof s.child === 'function'
  187. ? s.child(spanProps)
  188. : s.startChild(spanProps)
  189. )
  190. : [parentStore.get(parentName).child(spanProps)]
  191. : [this.transaction.startChild(spanProps)];
  192. spans.push(...span);
  193. // If we are starting a test, let's also make it a transaction so we can see our slowest tests
  194. if (spanProps.op === 'test') {
  195. spans.push(
  196. Sentry.startTransaction({
  197. ...spanProps,
  198. op: 'jest test',
  199. name: spanProps.description,
  200. description: null,
  201. })
  202. );
  203. }
  204. dataStore.set(testName, spans);
  205. return;
  206. }
  207. if (dataStore.has(testName)) {
  208. const spans = dataStore.get(testName);
  209. spans.forEach(span => {
  210. if (beforeFinish) {
  211. span = beforeFinish(span);
  212. if (!span) {
  213. throw new Error('`beforeFinish()` needs to return a span');
  214. }
  215. }
  216. span.finish();
  217. });
  218. }
  219. }
  220. }
  221. module.exports = SentryEnvironment;