123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262 |
- /* eslint-env node */
- const process = require('process'); // eslint-disable-line import/no-nodejs-modules
- // TODO: Make this configurable
- const JsDomEnvironment = require('@visual-snapshot/jest-environment');
- const Sentry = require('@sentry/node');
- require('@sentry/tracing');
- function isNotTransaction(span) {
- return span.op !== 'jest test';
- }
- class SentryEnvironment extends JsDomEnvironment {
- constructor(config, context) {
- super(config, context);
- if (!config.testEnvironmentOptions || !config.testEnvironmentOptions.SENTRY_DSN) {
- return;
- }
- this.Sentry = Sentry;
- this.Sentry.init({
- dsn: config.testEnvironmentOptions.SENTRY_DSN,
- tracesSampleRate: 1.0,
- environment: !!process.env.CI ? 'ci' : 'local',
- });
- this.testPath = context.testPath.replace(process.cwd(), '');
- this.runDescribe = new Map();
- this.testContainers = new Map();
- this.tests = new Map();
- this.hooks = new Map();
- }
- async setup() {
- if (!this.Sentry) {
- await super.setup();
- return;
- }
- this.transaction = Sentry.startTransaction({
- op: 'jest test suite',
- description: this.testPath,
- name: this.testPath,
- tags: {
- branch: process.env.GITHUB_REF,
- commit: process.env.GITHUB_SHA,
- },
- });
- Sentry.configureScope(scope => scope.setSpan(this.transaction));
- const span = this.transaction.startChild({
- op: 'setup',
- description: this.testPath,
- });
- await super.setup();
- span.finish();
- }
- async teardown() {
- if (!this.Sentry) {
- await super.teardown();
- return;
- }
- const span = this.transaction.startChild({
- op: 'teardown',
- description: this.testPath,
- });
- await super.teardown();
- span.finish();
- if (this.transaction) {
- this.transaction.finish();
- }
- this.runDescribe = null;
- this.testContainers = null;
- this.tests = null;
- this.hooks = null;
- this.hub = null;
- this.Sentry = null;
- }
- runScript(script) {
- // We are intentionally not instrumenting this as it will produce hundreds of spans.
- return super.runScript(script);
- }
- getName(parent) {
- if (!parent) {
- return '';
- }
- // Ignore these for now as it adds a level of nesting and I'm not quite sure where it's even coming from
- if (parent.name === 'ROOT_DESCRIBE_BLOCK') {
- return '';
- }
- const parentName = this.getName(parent.parent);
- return `${parentName ? `${parentName} >` : ''} ${parent.name}`;
- }
- getData({name, ...event}) {
- switch (name) {
- case 'run_describe_start':
- case 'run_describe_finish':
- return {
- op: 'describe',
- obj: event.describeBlock,
- parentObj: event.describeBlock.parent,
- dataStore: this.runDescribe,
- parentStore: this.runDescribe,
- };
- case 'test_start':
- case 'test_done':
- return {
- op: 'test',
- obj: event.test,
- parentObj: event.test.parent,
- dataStore: this.testContainers,
- parentStore: this.runDescribe,
- beforeFinish: span => {
- span.setStatus(!event.test.errors.length ? 'ok' : 'unknown_error');
- return span;
- },
- };
- case 'test_fn_start':
- case 'test_fn_success':
- case 'test_fn_failure':
- return {
- op: 'test-fn',
- obj: event.test,
- parentObj: event.test,
- dataStore: this.tests,
- parentStore: this.testContainers,
- beforeFinish: span => {
- span.setStatus(!event.test.errors.length ? 'ok' : 'unknown_error');
- return span;
- },
- };
- case 'hook_start':
- return {
- obj: event.hook.parent,
- op: event.hook.type,
- dataStore: this.hooks,
- };
- case 'hook_success':
- case 'hook_failure':
- return {
- obj: event.hook.parent,
- parentObj: event.test && event.test.parent,
- dataStore: this.hooks,
- parentStore: this.testContainers,
- beforeFinish: span => {
- const parent = this.testContainers.get(this.getName(event.test));
- if (parent && !Array.isArray(parent)) {
- return parent.child(span);
- } else if (Array.isArray(parent)) {
- return parent.find(isNotTransaction).child(span);
- }
- return span;
- },
- };
- case 'start_describe_definition':
- case 'finish_describe_definition':
- case 'add_test':
- case 'add_hook':
- case 'run_start':
- case 'run_finish':
- case 'test_todo':
- case 'setup':
- case 'teardown':
- return null;
- default:
- return null;
- }
- }
- handleTestEvent(event) {
- if (!this.Sentry) {
- return;
- }
- const data = this.getData(event);
- const {name} = event;
- if (!data) {
- return;
- }
- const {obj, parentObj, dataStore, parentStore, op, description, beforeFinish} = data;
- const testName = this.getName(obj);
- if (name.includes('start')) {
- // Make this an option maybe
- if (!testName) {
- return;
- }
- const spans = [];
- const parentName = parentObj && this.getName(parentObj);
- const spanProps = {op, description: description || testName};
- const span =
- parentObj && parentStore.has(parentName)
- ? Array.isArray(parentStore.get(parentName))
- ? parentStore
- .get(parentName)
- .map(s =>
- typeof s.child === 'function'
- ? s.child(spanProps)
- : s.startChild(spanProps)
- )
- : [parentStore.get(parentName).child(spanProps)]
- : [this.transaction.startChild(spanProps)];
- spans.push(...span);
- // If we are starting a test, let's also make it a transaction so we can see our slowest tests
- if (spanProps.op === 'test') {
- spans.push(
- Sentry.startTransaction({
- ...spanProps,
- op: 'jest test',
- name: spanProps.description,
- description: null,
- })
- );
- }
- dataStore.set(testName, spans);
- return;
- }
- if (dataStore.has(testName)) {
- const spans = dataStore.get(testName);
- spans.forEach(span => {
- if (beforeFinish) {
- span = beforeFinish(span);
- if (!span) {
- throw new Error('`beforeFinish()` needs to return a span');
- }
- }
- span.finish();
- });
- }
- }
- }
- module.exports = SentryEnvironment;
|