diff --git a/core/server/sentry.js b/core/server/sentry.js new file mode 100644 index 0000000000..927675c1cd --- /dev/null +++ b/core/server/sentry.js @@ -0,0 +1,30 @@ +const config = require('ghost-ignition').config(); +const sentryConfig = config.get('sentry'); + +const expressNoop = function (req, res, next) { + next(); +}; + +if (sentryConfig && !sentryConfig.disabled) { + const Sentry = require('@sentry/node'); + const version = require('../../package.json').version; + Sentry.init({ + dsn: sentryConfig.dsn, + release: 'ghost@' + version + }); + + module.exports = { + requestHandler: Sentry.Handlers.requestHandler(), + errorHandler: Sentry.Handlers.errorHandler({ + shouldHandleError(error) { + // Only handle 500 errors for now + return (error.statusCode === 500); + } + }) + }; +} else { + module.exports = { + requestHandler: expressNoop, + errorHandler: expressNoop + }; +} diff --git a/core/server/web/admin/app.js b/core/server/web/admin/app.js index 7f6b20b9d1..c5b0ce18e8 100644 --- a/core/server/web/admin/app.js +++ b/core/server/web/admin/app.js @@ -6,10 +6,12 @@ const constants = require('../../lib/constants'); const urlUtils = require('../../lib/url-utils'); const shared = require('../shared'); const adminMiddleware = require('./middleware'); +const sentry = require('../../sentry'); module.exports = function setupAdminApp() { debug('Admin setup start'); const adminApp = express(); + adminApp.use(sentry.requestHandler); // Make sure 'req.secure' and `req.hostname` is valid for proxied requests // (X-Forwarded-Proto header will be checked, if present) @@ -50,6 +52,7 @@ module.exports = function setupAdminApp() { // Finally, routing adminApp.get('*', require('./controller')); + adminApp.use(sentry.errorHandler); adminApp.use(shared.middlewares.errorHandler.pageNotFound); adminApp.use(shared.middlewares.errorHandler.handleHTMLResponse); diff --git a/core/server/web/api/canary/admin/app.js b/core/server/web/api/canary/admin/app.js index 82d1cf99b7..a085a63c41 100644 --- a/core/server/web/api/canary/admin/app.js +++ b/core/server/web/api/canary/admin/app.js @@ -4,10 +4,12 @@ const express = require('express'); const bodyParser = require('body-parser'); const shared = require('../../../shared'); const routes = require('./routes'); +const sentry = require('../../../../sentry'); module.exports = function setupApiApp() { debug('Admin API canary setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // API middleware @@ -32,6 +34,7 @@ module.exports = function setupApiApp() { apiApp.use(routes()); // API error handling + apiApp.use(sentry.errorHandler); apiApp.use(shared.middlewares.errorHandler.resourceNotFound); apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2); diff --git a/core/server/web/api/canary/content/app.js b/core/server/web/api/canary/content/app.js index 0a1cc727a5..d89bca694a 100644 --- a/core/server/web/api/canary/content/app.js +++ b/core/server/web/api/canary/content/app.js @@ -4,10 +4,12 @@ const bodyParser = require('body-parser'); const express = require('express'); const shared = require('../../../shared'); const routes = require('./routes'); +const sentry = require('../../../../sentry'); module.exports = function setupApiApp() { debug('Content API canary setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // API middleware @@ -27,6 +29,7 @@ module.exports = function setupApiApp() { apiApp.use(routes()); // API error handling + apiApp.use(sentry.errorHandler); apiApp.use(shared.middlewares.errorHandler.resourceNotFound); apiApp.use(shared.middlewares.errorHandler.handleJSONResponse); diff --git a/core/server/web/api/canary/members/app.js b/core/server/web/api/canary/members/app.js index 460677f2da..6c0816cbcf 100644 --- a/core/server/web/api/canary/members/app.js +++ b/core/server/web/api/canary/members/app.js @@ -6,10 +6,12 @@ const membersService = require('../../../../services/members'); const urlUtils = require('../../../../lib/url-utils'); const labs = require('../../../shared/middlewares/labs'); const shared = require('../../../shared'); +const sentry = require('../../../../sentry'); module.exports = function setupMembersApiApp() { debug('Members API canary setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // Entire app is behind labs flag apiApp.use(labs.members); @@ -24,6 +26,7 @@ module.exports = function setupMembersApiApp() { apiApp.put('/subscriptions/:id', (req, res, next) => membersService.api.middleware.updateSubscription(req, res, next)); // API error handling + apiApp.use(sentry.errorHandler); apiApp.use(shared.middlewares.errorHandler.resourceNotFound); apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2); diff --git a/core/server/web/api/index.js b/core/server/web/api/index.js index 3aeb08221e..4e3c1506e2 100644 --- a/core/server/web/api/index.js +++ b/core/server/web/api/index.js @@ -2,10 +2,12 @@ const debug = require('ghost-ignition').debug('web:api:default:app'); const express = require('express'); const urlUtils = require('../../lib/url-utils'); const errorHandler = require('../shared/middlewares/error-handler'); +const sentry = require('../../sentry'); module.exports = function setupApiApp() { debug('Parent API setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // Mount different API versions apiApp.use(urlUtils.getVersionPath({version: 'v2', type: 'content'}), require('./v2/content/app')()); @@ -20,6 +22,7 @@ module.exports = function setupApiApp() { apiApp.use(urlUtils.getVersionPath({version: 'canary', type: 'members'}), require('./canary/members/app')()); // Error handling for requests to non-existent API versions + apiApp.use(sentry.errorHandler); apiApp.use(errorHandler.resourceNotFound); apiApp.use(errorHandler.handleJSONResponse); diff --git a/core/server/web/api/v2/admin/app.js b/core/server/web/api/v2/admin/app.js index d46571e22d..db391204bd 100644 --- a/core/server/web/api/v2/admin/app.js +++ b/core/server/web/api/v2/admin/app.js @@ -4,10 +4,12 @@ const express = require('express'); const bodyParser = require('body-parser'); const shared = require('../../../shared'); const routes = require('./routes'); +const sentry = require('../../../../sentry'); module.exports = function setupApiApp() { debug('Admin API v2 setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // API middleware @@ -32,6 +34,7 @@ module.exports = function setupApiApp() { apiApp.use(routes()); // API error handling + apiApp.use(sentry.errorHandler); apiApp.use(shared.middlewares.errorHandler.resourceNotFound); apiApp.use(shared.middlewares.errorHandler.handleJSONResponseV2); diff --git a/core/server/web/api/v2/content/app.js b/core/server/web/api/v2/content/app.js index 0468649ba7..b2eb568294 100644 --- a/core/server/web/api/v2/content/app.js +++ b/core/server/web/api/v2/content/app.js @@ -4,10 +4,12 @@ const bodyParser = require('body-parser'); const express = require('express'); const shared = require('../../../shared'); const routes = require('./routes'); +const sentry = require('../../../../sentry'); module.exports = function setupApiApp() { debug('Content API v2 setup start'); const apiApp = express(); + apiApp.use(sentry.requestHandler); // API middleware @@ -27,6 +29,7 @@ module.exports = function setupApiApp() { apiApp.use(routes()); // API error handling + apiApp.use(sentry.errorHandler); apiApp.use(shared.middlewares.errorHandler.resourceNotFound); apiApp.use(shared.middlewares.errorHandler.handleJSONResponse); diff --git a/core/server/web/parent-app.js b/core/server/web/parent-app.js index 4fe865f1d5..0180c87838 100644 --- a/core/server/web/parent-app.js +++ b/core/server/web/parent-app.js @@ -9,12 +9,14 @@ const escapeRegExp = require('lodash.escaperegexp'); const {URL} = require('url'); const urlUtils = require('../lib/url-utils'); const storage = require('../adapters/storage'); +const sentry = require('../sentry'); const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`; module.exports = function setupParentApp(options = {}) { debug('ParentApp setup start'); const parentApp = express(); + parentApp.use(sentry.requestHandler); // ## Global settings @@ -53,6 +55,7 @@ module.exports = function setupParentApp(options = {}) { // Wrap the admin and API apps into a single express app for use with vhost const adminApp = express(); + adminApp.use(sentry.requestHandler); adminApp.enable('trust proxy'); // required to respect x-forwarded-proto in admin requests adminApp.use('/ghost/api', require('./api')()); adminApp.use('/ghost', require('./admin')()); diff --git a/core/server/web/site/app.js b/core/server/web/site/app.js index e8bad9709c..01ec048f96 100644 --- a/core/server/web/site/app.js +++ b/core/server/web/site/app.js @@ -19,6 +19,7 @@ const membersService = require('../../services/members'); const membersMiddleware = membersService.middleware; const siteRoutes = require('./routes'); const shared = require('../shared'); +const sentry = require('../../sentry'); const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`; @@ -76,6 +77,7 @@ module.exports = function setupSiteApp(options = {}) { debug('Site setup start'); const siteApp = express(); + siteApp.use(sentry.requestHandler); // Make sure 'req.secure' is valid for proxied requests // (X-Forwarded-Proto header will be checked, if present) @@ -193,6 +195,7 @@ module.exports = function setupSiteApp(options = {}) { siteApp.use(SiteRouter); // ### Error handlers + siteApp.use(sentry.errorHandler); siteApp.use(shared.middlewares.errorHandler.pageNotFound); siteApp.use(shared.middlewares.errorHandler.handleThemeResponse); diff --git a/index.js b/index.js index b0051b8b5b..48705d59be 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ var startTime = Date.now(), debug = require('ghost-ignition').debug('boot:index'), + sentry = require('./core/server/sentry'), ghost, express, common, urlService, parentApp; debug('First requires...'); @@ -16,6 +17,8 @@ common = require('./core/server/lib/common'); urlService = require('./core/frontend/services/url'); parentApp = express(); +parentApp.use(sentry.requestHandler); + debug('Initialising Ghost'); ghost().then(function (ghostServer) { // Mount our Ghost instance on our desired subdirectory path if it exists. diff --git a/package.json b/package.json index 54d1304a6c..a8b91e2bed 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@nexes/nql": "0.3.0", + "@sentry/node": "5.11.1", "@tryghost/helpers": "1.1.21", "@tryghost/members-api": "0.11.2", "@tryghost/members-ssr": "0.7.4", diff --git a/yarn.lock b/yarn.lock index be6b18afe0..71a83fe861 100644 --- a/yarn.lock +++ b/yarn.lock @@ -148,6 +148,85 @@ component-type "^1.2.1" join-component "^1.1.0" +"@sentry/apm@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.11.1.tgz#cc89fa4150056fbf009f92eca94fccc3980db34e" + integrity sha512-4iZH11p/7w9IMLT9hqNY1+EqLESltiIoF6/YsbpK93sXWGEs8VQ83IuvGuKWxajvHgDmj4ND0TxIliTsYqTqFw== + dependencies: + "@sentry/browser" "5.11.1" + "@sentry/hub" "5.11.1" + "@sentry/minimal" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/browser@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.1.tgz#337ffcb52711b23064c847a07629e966f54a5ebb" + integrity sha512-oqOX/otmuP92DEGRyZeBuQokXdeT9HQRxH73oqIURXXNLMP3PWJALSb4HtT4AftEt/2ROGobZLuA4TaID6My/Q== + dependencies: + "@sentry/core" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/core@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.1.tgz#9e2da485e196ae32971545c1c49ee6fe719930e2" + integrity sha512-BpvPosVNT20Xso4gAV54Lu3KqDmD20vO63HYwbNdST5LUi8oYV4JhvOkoBraPEM2cbBwQvwVcFdeEYKk4tin9A== + dependencies: + "@sentry/hub" "5.11.1" + "@sentry/minimal" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/hub@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.1.tgz#ddcb865563fae53852d405885c46b4c6de68a91b" + integrity sha512-ucKprYCbGGLLjVz4hWUqHN9KH0WKUkGf5ZYfD8LUhksuobRkYVyig0ZGbshECZxW5jcDTzip4Q9Qimq/PkkXBg== + dependencies: + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + tslib "^1.9.3" + +"@sentry/minimal@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.1.tgz#0e705d01a567282d8fbbda2aed848b4974cc3cec" + integrity sha512-HK8zs7Pgdq7DsbZQTThrhQPrJsVWzz7MaluAbQA0rTIAJ3TvHKQpsVRu17xDpjZXypqWcKCRsthDrC4LxDM1Bg== + dependencies: + "@sentry/hub" "5.11.1" + "@sentry/types" "5.11.0" + tslib "^1.9.3" + +"@sentry/node@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.11.1.tgz#2a9c18cd1209cfdf7a69b9d91303413149d2c910" + integrity sha512-FbJs0blJ36gEzE0rc2yBfA/KE+kXOLl8MUfFTcyJCBdCGF8XMETDCmgINnJ4TyBUJviwKoPw2TCk9TL2pa/A1w== + dependencies: + "@sentry/apm" "5.11.1" + "@sentry/core" "5.11.1" + "@sentry/hub" "5.11.1" + "@sentry/types" "5.11.0" + "@sentry/utils" "5.11.1" + cookie "^0.3.1" + https-proxy-agent "^4.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/types@5.11.0": + version "5.11.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.11.0.tgz#40f0f3174362928e033ddd9725d55e7c5cb7c5b6" + integrity sha512-1Uhycpmeo1ZK2GLvrtwZhTwIodJHcyIS6bn+t4IMkN9MFoo6ktbAfhvexBDW/IDtdLlCGJbfm8nIZerxy0QUpg== + +"@sentry/utils@5.11.1": + version "5.11.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.11.1.tgz#aa19fcc234cf632257b2281261651d2fac967607" + integrity sha512-O0Zl4R2JJh8cTkQ8ZL2cDqGCmQdpA5VeXpuBbEl1v78LQPkBDISi35wH4mKmLwMsLBtTVpx2UeUHBj0KO5aLlA== + dependencies: + "@sentry/types" "5.11.0" + tslib "^1.9.3" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -431,6 +510,11 @@ agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + agent-base@~4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" @@ -911,12 +995,12 @@ bluebird@3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== -bluebird@3.7.2: +bluebird@3.7.2, bluebird@^3.4.3, bluebird@^3.7.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bluebird@^3.4.1, bluebird@^3.4.3, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.3, bluebird@^3.5.4, bluebird@^3.5.5, bluebird@^3.7.0: +bluebird@^3.4.1, bluebird@^3.4.6, bluebird@^3.5.0, bluebird@^3.5.3, bluebird@^3.5.4, bluebird@^3.5.5: version "3.7.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.0.tgz#56a6a886e03f6ae577cffedeb524f8f2450293cf" integrity sha512-aBQ1FxIa7kSWCcmKHlcHFlT2jt6J/l4FzC7KcPELkOJOsPOb/bccdhmIrKDfXhwFrmc7vDoDrrepFvGqjyXGJg== @@ -1758,6 +1842,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + cookiejar@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" @@ -4161,6 +4250,14 @@ https-proxy-agent@^3.0.0: agent-base "^4.3.0" debug "^3.1.0" +https-proxy-agent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -5371,6 +5468,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" + integrity sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0= + mailcomposer@~0.2.10: version "0.2.12" resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-0.2.12.tgz#4d02a604616adcb45fb36d37513f4c1bd0b75681" @@ -8821,7 +8923,7 @@ truncate@~2.1.0: resolved "https://registry.yarnpkg.com/truncate/-/truncate-2.1.0.tgz#391183563a25cffbd4d613a1d00ae5844c9e55d3" integrity sha512-em3E3SUDONOjTBcZ36DTm3RvDded3IRU9rX32oHwwXNt3rJD5MVaFlJTQvs8tJoHRoeYP36OuQ1eL/Q7bNEWIQ== -tslib@^1.9.0: +tslib@^1.9.0, tslib@^1.9.3: version "1.10.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==