Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,531 changes: 975 additions & 556 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
"scripts": {
"prepare": "husky",
"dev": "docker compose -f docker-compose-real-backend-minus-frontend.yml up -d && npm run start:local",
"start": "node index.js",
"start": "node --import ./src/serverSetup/sentry.js index.js",
"start:watch": "concurrently \"nodemon --exec 'npm run build && npm run start'\" \"npm run scss:watch\"",
"start:test": "NODE_ENV=test node index.js",
"start:local": "NODE_ENV=local node index.js",
"start:wiremock": "docker compose up -d --force-recreate --build && NODE_ENV=wiremock node index.js",
"start:development": "NODE_ENV=development node index.js",
"start:test": "NODE_ENV=test node --import ./src/serverSetup/sentry.js index.js",
"start:local": "NODE_ENV=local node --import ./src/serverSetup/sentry.js index.js",
"start:wiremock": "docker compose up -d --force-recreate --build && NODE_ENV=wiremock node --import ./src/serverSetup/sentry.js index.js",
"start:development": "NODE_ENV=development node --import ./src/serverSetup/sentry.js index.js",
"start:local:watch": "NODE_ENV=test npm run start:watch",
"docker-security-scan": "mkdir -p zap-working-dir && touch zap-working-dir/zap.log && chmod -R a+rw zap-working-dir && docker compose -f docker-compose.security.yml run --rm zap",
"static-security-scan": "npm audit --json | tee npm-audit-report.json | npm-audit-markdown --output npm-audit-report.md || true",
Expand Down Expand Up @@ -51,6 +51,7 @@
"@vitest/coverage-v8": "^2.1.2",
"@wiremock/wiremock-testcontainers-node": "^0.0.1",
"concurrently": "^8.2.2",
"esbuild": "^0.27.3",
"husky": "^9.0.11",
"jsdoc": "^4.0.4",
"jsdoc-tsimport-plugin": "^1.0.5",
Expand All @@ -66,13 +67,12 @@
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2",
"zaproxy": "^2.0.0-rc.5",
"esbuild": "^0.27.3"
"zaproxy": "^2.0.0-rc.5"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.537.0",
"@sentry/node": "^8.7.0",
"@sentry/profiling-node": "^8.7.0",
"@sentry/node": "^10.47.0",
"@sentry/profiling-node": "^10.47.0",
"@x-govuk/govuk-prototype-components": "^3.0.5",
"@x-govuk/govuk-prototype-filters": "^1.4.3",
"accessible-autocomplete": "^3.0.0",
Expand Down
9 changes: 9 additions & 0 deletions src/controllers/resultsController.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/node'
import * as v from 'valibot'
import config from '../../config/index.js'
import PageController from './pageController.js'
Expand Down Expand Up @@ -71,11 +72,19 @@ export async function getRequestDataMiddleware (req, res, next) {
}

export async function checkForErroredResponse (req, res, next) {
if (!req.locals.requestData.response?.error) {
Sentry.metrics.count('url_submission.success', 1)
}

if (req.locals.requestData.response?.error) {
const { errMsg } = req.locals.requestData.response.error
if (errMsg && errMsg.length > 0) {
Sentry.metrics.count('url_submission.async_processing_failure', 1, { attributes: { error_message: errMsg } })
// Disable as this is not an error we want to track in Sentry, only need metrics
Sentry.getCurrentScope().setTag('async_handled_processing_error', true)
return next(new MiddlewareError(errMsg, 500, { template: 'check/error-redirect.html' }))
} else {
Sentry.metrics.count('url_submission.async_processing_failure', 1, { attributes: { error_message: 'unknown' } })
return next(new MiddlewareError('An unknown error occurred when processing your endpoint', 500, { template: 'check/error-redirect.html' }))
}
}
Expand Down
31 changes: 17 additions & 14 deletions src/controllers/submitUrlController.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const HTTP_STATUS_BLOCKED = 403

class SubmitUrlController extends UploadController {
async post (req, res, next) {
Sentry.metrics.count('url_submission.begun', 1)
const localValidationErrorType = await SubmitUrlController.localUrlValidation(req.body.url)

if (localValidationErrorType) {
Expand All @@ -33,14 +34,17 @@ class SubmitUrlController extends UploadController {
return next(errors)
}

const url = (req.body?.url ?? '').trim()
try {
const url = (req.body?.url ?? '').trim()
const id = await postUrlRequest({ ...this.getBaseFormData(req), url })
req.body.request_id = id
} catch (error) {
Sentry.metrics.count('url_submission.async_request_failure', 1, { attributes: { error_code: error.code, response_status: error.response?.status } })
logger.warn({ message: 'url_submission.async_request_failure', event: 'url_submission_failure', type: types.External, submittedUrl: url, errorMessage: error.message })
next(error)
return
}
Sentry.metrics.count('url_submission.accepted', 1)
super.post(req, res, next)
}

Expand All @@ -57,6 +61,8 @@ class SubmitUrlController extends UploadController {
]
const preCheckFailure = validators.find(validator => !validator.fn())
if (preCheckFailure) {
Sentry.metrics.count('url_submission.validation_failure', 1, { attributes: { failure_type: preCheckFailure.type } })
logger.warn({ message: 'url_submission.validation_failure', event: 'url_submission_failure', type: types.DataValidation, failure_type: preCheckFailure.type, submittedUrl: url })
return preCheckFailure.type
}

Expand All @@ -70,22 +76,24 @@ class SubmitUrlController extends UploadController {
const headResponse = await SubmitUrlController.headRequest(url)

if (!headResponse) {
logger.warn('submitUrlController/localUrlValidation: failed to get the submitted urls head, skipping post validators', {
type: types.DataFetch
})
Sentry.metrics.count('url_submission.head_request_error', 1, { attributes: { reason: 'network_error' } })
logger.warn({ message: 'url_submission.head_request_error', event: 'url_submission_failure', type: types.DataFetch, reason: 'network_error', submittedUrl: url })
return null
}

// 405 skip post validators
if (headResponse?.status === HTTP_STATUS_METHOD_NOT_ALLOWED) {
// HEAD request not allowed, return null or a specific error message
logger.warn('submitUrlController/localUrlValidation: failed to get the submitted urls head as it was not allowed (405) skipping post validators', {
type: types.DataFetch
})
Sentry.metrics.count('url_submission.head_request_error', 1, { attributes: { reason: 'method_not_allowed' } })
logger.warn({ message: 'url_submission.head_request_error', event: 'url_submission_failure', type: types.DataFetch, reason: 'method_not_allowed', submittedUrl: url })
return null
}

return postValidators(headResponse).find(validator => !validator.fn())?.type
const postCheckFailure = postValidators(headResponse).find(validator => !validator.fn())
if (postCheckFailure) {
Sentry.metrics.count('url_submission.validation_failure', 1, { attributes: { failure_type: postCheckFailure.type } })
logger.warn({ message: 'url_submission.validation_failure', event: 'url_submission_failure', type: types.DataValidation, failure_type: postCheckFailure.type, submittedUrl: url })
}
return postCheckFailure?.type
}

static urlIsDefined (url) {
Expand Down Expand Up @@ -128,11 +136,7 @@ class SubmitUrlController extends UploadController {
return await axios.head(url, axiosConfig)
} catch (err) {
const response = err?.response
const tags = { code: err.code, url }
if (response) {
tags.responseStatus = response.status
Sentry.metrics.increment('SubmitUrlController.headRequest: error', 1, { tags })

// If HEAD returns 403, try a GET request with Range header (signed URLs often don't support HEAD)
if (response.status === HTTP_STATUS_BLOCKED) {
try {
Expand Down Expand Up @@ -174,7 +178,6 @@ class SubmitUrlController extends UploadController {

return response
}
Sentry.metrics.increment('SubmitUrlController.headRequest: error', 1, { tags })
logger.info({ message: `SubmitUrlController.headRequest(): err.code=${err.code}`, type: types.App, url })
return null
}
Expand Down
30 changes: 20 additions & 10 deletions src/serverSetup/sentry.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import * as Sentry from '@sentry/node'
import { nodeProfilingIntegration } from '@sentry/profiling-node'
import dotenv from 'dotenv'

dotenv.config()

if (process.env.SENTRY_ENABLED?.toLowerCase() === 'true') {
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
enableTracing: process.env.SENTRY_TRACING_ENABLED?.toLowerCase() === 'true',
tracesSampleRate: parseFloat(process.env.SENTRY_TRACING_SAMPLE_RATE || '0.01'),
profilesSampleRate: parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE || '0.01'),
debug: process.env.SENTRY_DEBUG?.toLowerCase() === 'true',
release: process.env.GIT_COMMIT,
enableLogs: true,
beforeSend: (event) => {
if (event.tags?.async_handled_processing_error) return null
return event
}
})
}

const setupSentry = (app) => {
if (process.env.SENTRY_ENABLED?.toLowerCase() === 'true') {
Sentry.init({
dsn: process.env.SENTRY_DSN,
integrations: [nodeProfilingIntegration()],
enableTracing: process.env.SENTRY_TRACING_ENABLED?.toLowerCase() === 'true',
tracesSampleRate: parseFloat(process.env.SENTRY_TRACING_SAMPLE_RATE || '0.01'),
profilesSampleRate: parseFloat(process.env.SENTRY_PROFILES_SAMPLE_RATE || '0.01'),
debug: process.env.SENTRY_DEBUG?.toLowerCase() === 'true',
release: process.env.GIT_COMMIT
})

Sentry.setupExpressErrorHandler(app)
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/services/datasette.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios from 'axios'
import logger from '../utils/logger.js'
import { types } from '../utils/logging.js'
import config from '../../config/index.js'
import * as Sentry from '@sentry/node'

export default {
/**
Expand All @@ -25,7 +26,8 @@ export default {
formattedData: formatData(response.data.columns, response.data.rows)
}
} catch (error) {
logger.warn({ message: `runQuery(): ${error.message}`, type: types.App, query, datasetteUrl: config.datasetteUrl, database })
Sentry.metrics.count('datasette_query_errors', 1, { attributes: { url } })
logger.warn({ message: `runQuery(): ${error.message}`, type: types.App, query, datasetteUrl: config.datasetteUrl, database, queryUrl: url })
throw error
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/services/platformApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import axios from 'axios'
import logger from '../utils/logger.js'
import { types } from '../utils/logging.js'
import config from '../../config/index.js'
import * as Sentry from '@sentry/node'

/**
* Service for querying the Platform API (mainWebsiteUrl)
Expand Down Expand Up @@ -143,6 +144,7 @@ async function queryPlatformAPI (url, params = {}) {

return response.data
} catch (error) {
Sentry.metrics.count('platform_api_errors', 1, { attributes: { url } })
logger.warn({
message: `queryPlatformAPI(): ${error.message}`,
type: types.External,
Expand Down
14 changes: 12 additions & 2 deletions src/utils/logger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger, format, transports } from 'winston'
import { createLogger, format, transports, Transport } from 'winston'
import * as Sentry from '@sentry/node'
import config from '../../config/index.js'

/* eslint-disable no-unused-vars */
Expand All @@ -9,9 +10,18 @@ const ignoreAssetRequests = format((info, opts) => {
return info
})

class SentryTransport extends Transport {
log (info, callback) {
if (info.level === 'warn') Sentry.logger.warn(info.message, info)
else if (info.level === 'error') Sentry.logger.error(info.message, info)
callback()
}
}

const appTransports = () => [
new transports.Console(),
new transports.File({ filename: 'combined.log' })
new transports.File({ filename: 'combined.log' }),
new SentryTransport({ level: 'warn' })
]

const testTransports = () => [
Expand Down
Loading