rittenhop-dev/versions/5.94.2/core/shared/sentry.js
2024-09-23 19:40:12 -04:00

179 lines
6.4 KiB
JavaScript

const config = require('./config');
const logging = require('@tryghost/logging');
const SentryKnexTracingIntegration = require('./SentryKnexTracingIntegration');
const sentryConfig = config.get('sentry');
const errors = require('@tryghost/errors');
/**
* @param {import('@sentry/node').Event} event
* @param {import('@sentry/node').EventHint} hint
* @returns {import('@sentry/node').Event | null}
*/
const beforeSend = function (event, hint) {
try {
const exception = hint.originalException;
const code = exception?.code ?? null;
const context = exception?.context ?? null;
const errorType = exception?.errorType ?? null;
const id = exception?.id ?? null;
const statusCode = exception?.statusCode ?? null;
event.tags = event.tags || {};
if (errors.utils.isGhostError(exception)) {
// Unexpected errors have a generic error message, set it back to context if there is one
if (code === 'UNEXPECTED_ERROR' && context !== null) {
if (event.exception.values?.length > 0) {
event.exception.values[0].type = context;
}
}
// This is a mysql2 error — add some additional context
if (exception.sql) {
const sql = exception.sql;
const errno = exception.errno ?? null;
const sqlErrorCode = exception.sqlErrorCode ?? null;
const sqlMessage = exception.sqlMessage ?? null;
const sqlState = exception.sqlState ?? null;
if (event.exception.values?.length > 0) {
event.exception.values[0].type = `SQL Error ${errno}: ${sqlErrorCode}`;
event.exception.values[0].value = sqlMessage;
event.contexts = event.contexts || {};
event.contexts.mysql = {
errno: errno,
code: sqlErrorCode,
sql: sql,
message: sqlMessage,
state: sqlState
};
}
}
// This is a Ghost Error, copy all our extra data to tags
event.tags.type = errorType;
event.tags.code = code;
event.tags.id = id;
event.tags.status_code = statusCode;
// Only handle 500 errors for now
// This is because the only other 5XX error should be 503, which are deliberate maintenance/boot errors
if (statusCode === 500) {
return event;
} else {
return null;
}
}
return event;
} catch (error) {
// If any errors occur in beforeSend, send the original event to Sentry
// Better to have some information than no information
return event;
}
};
const ALLOWED_HTTP_TRANSACTIONS = [
'/ghost/api', // any Ghost API call
'/members/api', // any Members API call
'/:slug', // any frontend post/page
'/author', // any frontend author page
'/tag' // any frontend tag page
].map((path) => {
// Sentry names HTTP transactions like: "<HTTP_METHOD> <PATH>" i.e. "GET /ghost/api/content/settings"
// Match any of the paths above with any HTTP method, and also the homepage "GET /"
return new RegExp(`^(GET|POST|PUT|DELETE)\\s(?<path>${path}\\/.+|\\/$)`);
});
/**
* @param {import('@sentry/node').Event} event
* @returns {import('@sentry/node').Event | null}
*/
const beforeSendTransaction = function (event) {
// Drop transactions that are not in the allowed list
for (const transaction of ALLOWED_HTTP_TRANSACTIONS) {
const match = event.transaction.match(transaction);
if (match?.groups?.path) {
return event;
}
}
return null;
};
if (sentryConfig && !sentryConfig.disabled) {
const Sentry = require('@sentry/node');
const version = require('@tryghost/version').full;
let environment = config.get('PRO_ENV');
if (!environment) {
environment = config.get('env');
}
/** @type {import('@sentry/node').NodeOptions} */
const sentryInitConfig = {
dsn: sentryConfig.dsn,
release: 'ghost@' + version,
environment: environment,
maxValueLength: 1000,
integrations: [
Sentry.extraErrorDataIntegration()
],
beforeSend,
beforeSendTransaction
};
// Enable tracing if sentry.tracing.enabled is true
if (sentryConfig.tracing?.enabled === true) {
sentryInitConfig.integrations.push(new Sentry.Integrations.Http({tracing: true}));
sentryInitConfig.tracesSampleRate = parseFloat(sentryConfig.tracing.sampleRate) || 0.0;
// Enable profiling, if configured, only if tracing is also configured
if (sentryConfig.profiling?.enabled === true) {
// Import Sentry's profiling integration if available
let ProfilingIntegration;
try {
({ProfilingIntegration} = require('@sentry/profiling-node'));
} catch (err) {
logging.warn('Sentry Profiling Integration not available');
ProfilingIntegration = null;
}
if (ProfilingIntegration) {
sentryInitConfig.integrations.push(new ProfilingIntegration());
sentryInitConfig.profilesSampleRate = parseFloat(sentryConfig.profiling.sampleRate) || 0.0;
}
}
}
Sentry.init(sentryInitConfig);
module.exports = {
requestHandler: Sentry.Handlers.requestHandler(),
errorHandler: Sentry.Handlers.errorHandler(),
tracingHandler: Sentry.Handlers.tracingHandler(),
captureException: Sentry.captureException,
captureMessage: Sentry.captureMessage,
beforeSend: beforeSend,
beforeSendTransaction: beforeSendTransaction,
initQueryTracing: (knex) => {
if (sentryConfig.tracing?.enabled === true) {
const integration = new SentryKnexTracingIntegration(knex);
Sentry.addIntegration(integration);
}
}
};
} else {
const expressNoop = function (req, res, next) {
next();
};
const noop = () => {};
module.exports = {
requestHandler: expressNoop,
errorHandler: expressNoop,
tracingHandler: expressNoop,
captureException: noop,
initQueryTracing: noop
};
}