diff --git a/forge/forge.js b/forge/forge.js index 3f7b5bbfc2..aae8ef5d61 100644 --- a/forge/forge.js +++ b/forge/forge.js @@ -1,7 +1,7 @@ const cookie = require('@fastify/cookie') const csrf = require('@fastify/csrf-protection') const helmet = require('@fastify/helmet') -const { ProfilingIntegration } = require('@sentry/profiling-node') +const Sentry = require('@sentry/node') const fastify = require('fastify') const auditLog = require('./auditLog') @@ -139,6 +139,15 @@ module.exports = async (options = {}) => { } } } + if (runtimeConfig.telemetry.backend?.sentry?.dsn) { + Sentry.init({ + dsn: runtimeConfig.telemetry.backend.sentry.dsn, + sendClientReports: true, + environment: process.env.SENTRY_ENV ?? (process.env.NODE_ENV ?? 'unknown'), + release: `flowfuse@${runtimeConfig.version}` + }) + } + const server = fastify({ forceCloseConnections: true, bodyLimit: 10485760, // 10mb max payload size, set to allow for VERY large flows @@ -151,75 +160,15 @@ module.exports = async (options = {}) => { pluginTimeout: 20000 }) + if (runtimeConfig.telemetry.backend?.sentry?.dsn) { + Sentry.setupFastifyErrorHandler(server) + } + if (runtimeConfig.telemetry.backend?.prometheus?.enabled) { const metricsPlugin = require('fastify-metrics') await server.register(metricsPlugin, { endpoint: '/metrics' }) } - if (runtimeConfig.telemetry.backend?.sentry?.dsn) { - const environment = process.env.SENTRY_ENV ?? (process.env.NODE_ENV ?? 'unknown') - const sentrySampleRate = environment === 'production' ? 0.1 : 0.5 - server.register(require('@immobiliarelabs/fastify-sentry'), { - dsn: runtimeConfig.telemetry.backend.sentry.dsn, - sendClientReports: true, - environment, - release: `flowfuse@${runtimeConfig.version}`, - profilesSampleRate: sentrySampleRate, // relative to output from tracesSampler - integrations: [ - new ProfilingIntegration() - ], - extractUserData (request) { - const user = request.session?.User || request.user - if (!user) { - return {} - } - const extractedUser = { - id: user.hashid, - username: user.username, - email: user.email, - name: user.name - } - - return extractedUser - }, - tracesSampler: (samplingContext) => { - // Adjust sample rates for routes with high volumes, sorted descending by volume - - // Used for mosquitto auth - if (samplingContext?.transactionContext?.name === 'POST /api/comms/auth/client' || samplingContext?.transactionContext?.name === 'POST /api/comms/auth/acl') { - return 0.001 - } - - // Used by nr-launcher and for nr-auth - if (samplingContext?.transactionContext?.name === 'GET POST /account/token') { - return 0.01 - } - - // Common endpoints in app (list devices by team, list devices by project) - if (samplingContext?.transactionContext?.name === 'GET /api/v1/teams/:teamId/devices' || samplingContext?.transactionContext?.name === 'GET /api/v1/projects/:instanceId/devices') { - return 0.01 - } - - // Used by device editor device tunnel - if (samplingContext?.transactionContext?.name === 'GET /api/v1/devices/:deviceId/editor/proxy/*') { - return 0.01 - } - - // Prometheus scraping - if (samplingContext?.transactionContext?.name === 'GET /metrics') { - return 0.01 - } - - // OAuth check - if (samplingContext?.transactionContext?.name === 'GET /account/check/:ownerType/:ownerId') { - return 0.01 - } - - return sentrySampleRate - } - }) - } - server.addHook('onError', async (request, reply, error) => { // Useful for debugging when a route goes wrong // console.error(error.stack) diff --git a/forge/housekeeper/index.js b/forge/housekeeper/index.js index bf98f026a5..25d4f1d54d 100644 --- a/forge/housekeeper/index.js +++ b/forge/housekeeper/index.js @@ -1,4 +1,4 @@ -const { captureCheckIn, captureException } = require('@sentry/node') +const { captureException } = require('@sentry/node') const { scheduleTask } = require('cronosjs') const fp = require('fastify-plugin') @@ -41,56 +41,7 @@ module.exports = fp(async function (app, _opts) { clearInterval(voteInterval) }) - function reportTask (name, schedule) { - try { - return captureCheckIn({ - monitorSlug: name, - status: 'in_progress' - }, - { - schedule: { - type: 'crontab', - value: schedule - }, - checkinMargin: 5, - maxRuntime: 5, - timezone: 'Etc/UTC' - }) - } catch (error) { - app.log.warn('Failed to report to Sentry', error) - } - } - - function reportTaskComplete (checkInId, name) { - if (!checkInId) { - return - } - - try { - captureCheckIn({ - checkInId, - monitorSlug: name, - status: 'ok' - }) - } catch (error) { - app.log.warn('Failed to report task complete to Sentry', error) - } - } - - function reportTaskFailure (checkInId, name, errorMessage) { - if (checkInId) { - try { - captureCheckIn({ - checkInId, - monitorSlug: name, - status: 'error', - errorMessage - }) - } catch (error) { - app.log.warn('Failed to report task failure to Sentry', error) - } - } - + function reportTaskFailure (errorMessage) { try { captureException(new Error(errorMessage)) } catch (error) { @@ -119,16 +70,13 @@ module.exports = fp(async function (app, _opts) { if (checkVote()) { app.log.trace(`Running task '${task.name}'`) - const checkInId = reportTask(task.name, task.schedule) - return task .run(app) - .then(reportTaskComplete.bind(this, checkInId, task.name)) .catch(err => { const errorMessage = `Error running task '${task.name}: ${err.toString()}` app.log.error(errorMessage) - reportTaskFailure(checkInId, task.name, errorMessage) + reportTaskFailure(errorMessage) }).then(() => { app.log.trace(`Completed task '${task.name}'`) return null diff --git a/forge/routes/auth/index.js b/forge/routes/auth/index.js index 26328bdcb3..7b9f4175ad 100644 --- a/forge/routes/auth/index.js +++ b/forge/routes/auth/index.js @@ -1,3 +1,5 @@ +const Sentry = require('@sentry/node') + /** * Routes related to session handling, login/out etc * @@ -72,6 +74,7 @@ async function init (app, opts) { const mfaMissing = request.session.User.mfa_enabled && !request.session.mfa_verified if (emailVerified && passwordNotExpired && !suspended && !mfaMissing) { + Sentry.setUser({ id: request.session.User.hashid, username: request.session.User.username, email: request.session.User.email, name: request.session.User.name }) return } if (request.routeOptions.config.allowAnonymous) { @@ -116,6 +119,7 @@ async function init (app, opts) { reply.code(401).send({ code: 'unauthorized', error: 'unauthorized' }) return } + Sentry.setUser({ id: request.session.User.hashid, username: request.session.User.username, email: request.session.User.email, name: request.session.User.name }) if (accessToken.name) { // Temp hack to give token full user scope delete request.session.scope diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue index 1474463942..4ba73f3468 100644 --- a/frontend/src/components/PageHeader.vue +++ b/frontend/src/components/PageHeader.vue @@ -1,5 +1,5 @@