From e493efae2b0c192879020c6c3c2e4d8f5f4b9e11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Skr=C3=B8vseth?= Date: Thu, 1 Feb 2024 21:16:42 +0100 Subject: [PATCH] metrics --- server/package-lock.json | 22 ++- server/package.json | 4 +- server/src/config/env.ts | 2 +- server/src/functions/cookies.ts | 23 --- server/src/init.ts | 6 +- server/src/logger.ts | 20 +- server/src/middleware/redirect/gauges.ts | 32 ++++ .../{ => redirect}/redirect.test.ts | 180 +++++++++++++----- .../src/middleware/{ => redirect}/redirect.ts | 97 +++++++++- server/src/prometheus/middleware.ts | 3 +- server/src/prometheus/types.ts | 10 + server/src/routes/version.ts | 20 -- server/src/types/http.ts | 3 + 13 files changed, 307 insertions(+), 115 deletions(-) delete mode 100644 server/src/functions/cookies.ts create mode 100644 server/src/middleware/redirect/gauges.ts rename server/src/middleware/{ => redirect}/redirect.test.ts (52%) rename server/src/middleware/{ => redirect}/redirect.ts (63%) create mode 100644 server/src/prometheus/types.ts delete mode 100644 server/src/routes/version.ts diff --git a/server/package-lock.json b/server/package-lock.json index 4c954a1f..8d37c520 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@navikt/nav-dekoratoren-moduler": "2.1.5", "compression": "1.7.4", + "cookie": "^0.6.0", "cors": "2.8.5", "express": "4.18.2", "express-prom-bundle": "7.0.0", @@ -22,6 +23,7 @@ }, "devDependencies": { "@types/compression": "1.7.5", + "@types/cookie": "^0.6.0", "@types/cors": "2.8.17", "@types/jest": "29.5.11", "@typescript-eslint/eslint-plugin": "6.20.0", @@ -1490,6 +1492,12 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -2712,9 +2720,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -3880,6 +3888,14 @@ "prom-client": ">=15.0.0" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", diff --git a/server/package.json b/server/package.json index d12b16bd..05b6fe3b 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "dependencies": { "@navikt/nav-dekoratoren-moduler": "2.1.5", "compression": "1.7.4", + "cookie": "^0.6.0", "cors": "2.8.5", "express": "4.18.2", "express-prom-bundle": "7.0.0", @@ -27,6 +28,7 @@ }, "devDependencies": { "@types/compression": "1.7.5", + "@types/cookie": "^0.6.0", "@types/cors": "2.8.17", "@types/jest": "29.5.11", "@typescript-eslint/eslint-plugin": "6.20.0", @@ -46,4 +48,4 @@ "tsc-alias": "1.8.8", "typescript": "5.3.3" } -} \ No newline at end of file +} diff --git a/server/src/config/env.ts b/server/src/config/env.ts index 8afef5f4..30f2d847 100644 --- a/server/src/config/env.ts +++ b/server/src/config/env.ts @@ -28,7 +28,7 @@ export const DOMAIN: string = getEnvironmentVersion( `http://localhost:${serverConfig.port}`, `http://localhost:${serverConfig.port}`, 'https://klage.intern.dev.nav.no', - 'https://klage.intern.nav.no', + 'https://klage.nav.no', ); export const NAIS_NAMESPACE = requiredEnvString('NAIS_NAMESPACE', 'none'); diff --git a/server/src/functions/cookies.ts b/server/src/functions/cookies.ts deleted file mode 100644 index 77dbd520..00000000 --- a/server/src/functions/cookies.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Request } from '@app/types/http'; - -export const getCookie = (name: string, req: Request): string | null => { - const cookies = req.header('cookie')?.split(';'); - - if (typeof cookies === 'undefined' || cookies.length === 0) { - return null; - } - - const cookie = cookies.find((c) => c.startsWith(`${name}=`)); - - if (typeof cookie === 'undefined') { - return null; - } - - const parts = cookie.split('='); - - if (parts.length !== 2) { - return null; - } - - return parts[1] ?? null; -}; diff --git a/server/src/init.ts b/server/src/init.ts index fdac343a..53c49abc 100644 --- a/server/src/init.ts +++ b/server/src/init.ts @@ -1,12 +1,11 @@ import { Express, static as expressStatic } from 'express'; import { frontendDistDirectoryPath } from '@app/config/config'; -import { redirectMiddleware } from '@app/middleware/redirect'; +import { redirectMiddleware } from '@app/middleware/redirect/redirect'; import { serverConfig } from './config/server-config'; import { getLogger } from './logger'; import { appHandler } from './routes/app-handler'; import { errorReporter } from './routes/error-report'; import { setupProxy } from './routes/setup-proxy'; -import { setupVersionRoute } from './routes/version'; import { EmojiIcons, sendToSlack } from './slack'; const log = getLogger('init'); @@ -25,11 +24,10 @@ export const init = async (server: Express) => { } }); server.use(errorReporter()); - server.use(setupVersionRoute()); server.use(await setupProxy()); server.use(expressStatic(frontendDistDirectoryPath, { index: false, fallthrough: true })); server.use(redirectMiddleware); - server.get(['/nb*', '/en*'], appHandler); + server.get('*', appHandler); server.listen(PORT, () => log.info({ msg: `Listening on port ${PORT}` })); } catch (e) { if (e instanceof Error) { diff --git a/server/src/logger.ts b/server/src/logger.ts index 6e4801f6..150f265d 100644 --- a/server/src/logger.ts +++ b/server/src/logger.ts @@ -23,15 +23,15 @@ export interface AnyObject { type LogArgs = | { - msg?: string; - error: Error | unknown; - data?: SerializableValue | AnyObject; - } + msg?: string; + error: Error | unknown; + data?: SerializableValue | AnyObject; + } | { - msg: string; - error?: Error | unknown; - data?: SerializableValue | AnyObject; - }; + msg: string; + error?: Error | unknown; + data?: SerializableValue | AnyObject; + }; interface Logger { debug: (args: LogArgs) => void; @@ -110,8 +110,8 @@ export const httpLoggingMiddleware: RequestHandler = (req, res, next) => { res.once('finish', () => { const { method, url } = req; - const referrer = req.get('referrer'); - const userAgent = req.get('user-agent'); + const referrer = req.header('referrer'); + const userAgent = req.header('user-agent'); if (url.endsWith('/isAlive') || url.endsWith('/isReady')) { return; diff --git a/server/src/middleware/redirect/gauges.ts b/server/src/middleware/redirect/gauges.ts new file mode 100644 index 00000000..20889c37 --- /dev/null +++ b/server/src/middleware/redirect/gauges.ts @@ -0,0 +1,32 @@ +import { Gauge } from 'prom-client'; +import { registers } from '@app/prometheus/types'; + +export const anonymousViewCountGauge = new Gauge({ + name: 'anonymous_view_count', + help: 'Number of anonymous viewa.', + labelNames: ['url', 'lang', 'type', 'ytelse', 'subpage', 'has_saksnummer', 'redirected_from'], + registers, +}); + +export const loggedInViewCountGauge = new Gauge({ + name: 'logged_in_view_count', + help: 'Number of logged in views.', + labelNames: ['url', 'lang', 'type', 'id', 'subpage', 'has_saksnummer', 'redirected_from'], + registers, +}); + +const labelNames = ['from_url', 'to_url', 'has_saksnummer'] as const; + +export const externalRedirectGauge = new Gauge({ + name: 'external_redirect', + help: 'Number of redirects to nav.no/klage.', + labelNames, + registers, +}); + +export const internalRedirectGauge = new Gauge({ + name: 'internal_redirect', + help: 'Number of internal redirects to fix deprected paths.', + labelNames, + registers, +}); diff --git a/server/src/middleware/redirect.test.ts b/server/src/middleware/redirect/redirect.test.ts similarity index 52% rename from server/src/middleware/redirect.test.ts rename to server/src/middleware/redirect/redirect.test.ts index 633a1955..f9a522ef 100644 --- a/server/src/middleware/redirect.test.ts +++ b/server/src/middleware/redirect/redirect.test.ts @@ -1,199 +1,295 @@ -import { redirectMiddleware } from '@app/middleware/redirect'; +import { redirectMiddleware } from '@app/middleware/redirect/redirect'; import { Request, Response } from '@app/types/http'; describe('redirect', () => { it('should not redirect other methods than GET', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/klage/ny', 'POST'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/klage/ny'; + const req: Request = createRequest(url, 'POST'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).not.toHaveBeenCalled(); + expect(res.cookie).not.toHaveBeenCalled(); expect(next).toHaveBeenCalledWith(); }); it('should redirect requests to numeric IDs without suffix path', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/klage/123', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/klage/123'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/klage/123/begrunnelse'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalledWith(); }); it('should not redirect requests to numeric IDs with suffix path', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/klage/123/begrunnelse', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).not.toHaveBeenCalledWith(); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).toHaveBeenCalledWith(); }); it('should redirect requests to UUID IDs without suffix path', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/klage/acf74e14-c38f-4ad4-9759-9b63981d7ef9', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/klage/acf74e14-c38f-4ad4-9759-9b63981d7ef9'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/klage/acf74e14-c38f-4ad4-9759-9b63981d7ef9/begrunnelse'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalledWith(); }); it('should not redirect requests to UUID IDs with suffix path', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/klage/acf74e14-c38f-4ad4-9759-9b63981d7ef9/begrunnelse', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).not.toHaveBeenCalledWith(); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).toHaveBeenCalledWith(); }); it('should redirect to ytelse based on query params', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/anke?tema=YRK&tittel=YRKESSKADE', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/anke?tema=YRK&tittel=YRKESSKADE'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/anke/YRKESSKADE'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should preserve saksnummer on redirect', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/anke/ny/YRKESSKADE?saksnummer=123', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/anke/YRKESSKADE?saksnummer=123'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '/nb/anke/ny/YRKESSKADE?saksnummer=SAKSNUMMER', { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should redirect to path without /ny based on path param', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/anke/ny/YRKESSKADE', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/anke/ny/YRKESSKADE'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/anke/YRKESSKADE'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should redirect to path without /ny based on query params', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/anke/ny?tema=YRK&tittel=YRKESSKADE', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/anke/ny?tema=YRK&tittel=YRKESSKADE'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/anke/YRKESSKADE'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should redirect to path without /ny without type based on query params', () => { - expect.assertions(2); + expect.assertions(3); - const req: Request = createRequest('/nb/ny?tema=YRK&tittel=YRKESSKADE', 'GET'); - const res: Response = { redirect: jest.fn() }; + const url = '/nb/ny?tema=YRK&tittel=YRKESSKADE'; + const req: Request = createRequest(url, 'GET'); + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/klage/YRKESSKADE'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', url, { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should preserve saksnummer on redirect without type based on query params', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/ny?tema=YRK&tittel=YRKESSKADE&saksnummer=123', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, '/nb/klage/YRKESSKADE?saksnummer=123'); + expect(res.cookie).toHaveBeenCalledWith( + 'redirected_from', + '/nb/ny?tema=YRK&tittel=YRKESSKADE&saksnummer=SAKSNUMMER', + { + expires: undefined, + httpOnly: true, + maxAge: undefined, + sameSite: 'strict', + }, + ); expect(next).not.toHaveBeenCalled(); }); it('should redirect from /nb/klage/ny to nav.no/klage', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/klage/ny', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, 'https://www.ekstern.dev.nav.no/klage'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should redirect / to nav.no/klage', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).toHaveBeenCalledWith(301, 'https://www.ekstern.dev.nav.no/klage'); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).not.toHaveBeenCalled(); }); it('should not redirect /nb/ettersendelse/DAGPENGER', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/ettersendelse/DAGPENGER', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).not.toHaveBeenCalledWith(); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).toHaveBeenCalledWith(); }); it('should not redirect /nb/anke/uinnlogget/DAGPENGER/begrunnelse', () => { - expect.assertions(2); + expect.assertions(3); const req: Request = createRequest('/nb/anke/uinnlogget/DAGPENGER/begrunnelse', 'GET'); - const res: Response = { redirect: jest.fn() }; + const res: Response = { redirect: jest.fn(), cookie: jest.fn() }; const next = jest.fn(); redirectMiddleware(req, res, next); expect(res.redirect).not.toHaveBeenCalledWith(); + expect(res.cookie).toHaveBeenCalledWith('redirected_from', '', { + expires: new Date(0), + httpOnly: true, + maxAge: 0, + sameSite: 'strict', + }); expect(next).toHaveBeenCalledWith(); }); }); @@ -209,14 +305,8 @@ const createRequest = (url: string, method: string): Request => { url, path: pathname, method, - header: (name: string) => { - if (name === 'Accept-Language') { - return 'no-nb'; - } - - return undefined; - }, query: search === undefined ? {} : parseQueryParams(search), + header: () => undefined, }; }; diff --git a/server/src/middleware/redirect.ts b/server/src/middleware/redirect/redirect.ts similarity index 63% rename from server/src/middleware/redirect.ts rename to server/src/middleware/redirect/redirect.ts index b4b5d8e5..2d9dc033 100644 --- a/server/src/middleware/redirect.ts +++ b/server/src/middleware/redirect/redirect.ts @@ -1,13 +1,20 @@ +import { parse } from 'cookie'; import { isDeployedToProd } from '@app/config/env'; import { isInnsendingsytelse } from '@app/innsendingsytelser'; import { getLogger } from '@app/logger'; +import { + anonymousViewCountGauge, + externalRedirectGauge, + internalRedirectGauge, + loggedInViewCountGauge, +} from '@app/middleware/redirect/gauges'; import { Request, Response } from '@app/types/http'; const log = getLogger('redirect-middleware'); // eslint-disable-next-line complexity export const redirectMiddleware = (req: Request, res: Response, next: () => void) => { - const { path, method } = req; + const { path, method, url } = req; if (method !== 'GET') { return next(); @@ -22,7 +29,7 @@ export const redirectMiddleware = (req: Request, res: Response, next: () => void // /nb/klage/123/begrunnelse // /nb/klage/123 // /nb/klage/uinnlogget/NOT_A_YTELSE - const [lang, second, third, fourth, fith] = path.split('/').filter((p) => p.length !== 0); + const [lang, second, third, fourth, fifth] = path.split('/').filter((p) => p.length !== 0); if (lang === undefined || second === undefined) { return redirectToYtelseOverview(req, res); @@ -78,11 +85,23 @@ export const redirectMiddleware = (req: Request, res: Response, next: () => void // /nb/klage/uinnlogget/NOT_A_YTELSE if (third === 'uinnlogget') { if (isInnsendingsytelse(fourth)) { - if (fith === undefined) { + if (fifth === undefined) { // /nb/klage/uinnlogget/DAGPENGER -> /nb/klage/uinnlogget/DAGPENGER/begrunnelse return redirectToFixedUrl(req, res, `/${lang}/${second}/uinnlogget/${fourth}/begrunnelse`); } + anonymousViewCountGauge.inc({ + url: anonymizeSaksnummer(url), + lang, + type: second, + ytelse: third, + subpage: fifth, + has_saksnummer: getHasSaksnummer(req), + redirected_from: getRedirectedFrom(req), + }); + + deleteRedirectedCookie(res); + // /nb/klage/uinnlogget/DAGPENGER/begrunnelse return next(); } @@ -93,6 +112,18 @@ export const redirectMiddleware = (req: Request, res: Response, next: () => void if (isInnsendingsytelse(third)) { if (fourth === undefined) { + anonymousViewCountGauge.inc({ + url, + lang, + type: second, + ytelse: third, + subpage: 'NONE', + has_saksnummer: getHasSaksnummer(req), + redirected_from: getRedirectedFrom(req), + }); + + deleteRedirectedCookie(res); + // /nb/klage/DAGPENGER return next(); } @@ -106,6 +137,17 @@ export const redirectMiddleware = (req: Request, res: Response, next: () => void return redirectToFixedUrl(req, res, `/${lang}/${second}/${third}/begrunnelse`); } + loggedInViewCountGauge.inc({ + url, + lang, + type: second, + id: third, + subpage: fourth, + redirected_from: getRedirectedFrom(req), + }); + + deleteRedirectedCookie(res); + // /nb/klage/123/begrunnelse // /nb/klage/123/oppsummering // /nb/klage/123/innsending @@ -114,25 +156,66 @@ export const redirectMiddleware = (req: Request, res: Response, next: () => void // /nb/anke/acf74e14-c38f-4ad4-9759-9b63981d7ef9/oppsummering // /nb/anke/acf74e14-c38f-4ad4-9759-9b63981d7ef9/innsending // /nb/anke/acf74e14-c38f-4ad4-9759-9b63981d7ef9/kvittering - next(); + return next(); }; +const EXPIRED = new Date(0); + +const deleteRedirectedCookie = (res: Response) => + res.cookie('redirected_from', '', { maxAge: 0, expires: EXPIRED, httpOnly: true, sameSite: 'strict' }); + const YTELSE_OVERVIEW_URL = isDeployedToProd ? 'https://www.nav.no/klage' : 'https://www.ekstern.dev.nav.no/klage'; const redirectToYtelseOverview = (req: Request, res: Response) => { - log.warn({ msg: `Invalid URL. Redirecting to ${YTELSE_OVERVIEW_URL}`, data: { url: req.url } }); + const has_saksnummer = getHasSaksnummer(req); + const from_url = anonymizeSaksnummer(req.url); + log.warn({ + msg: `Invalid URL. Redirecting to ${YTELSE_OVERVIEW_URL}`, + data: { from_url, reason: 'invalid' }, + }); + externalRedirectGauge.inc({ from_url, to_url: YTELSE_OVERVIEW_URL, has_saksnummer }); + res.cookie('redirected_from', '', { maxAge: 0, expires: EXPIRED, httpOnly: true, sameSite: 'strict' }); res.redirect(301, YTELSE_OVERVIEW_URL); }; const redirectToFixedUrl = (req: Request, res: Response, path: string) => { const saksnummer = getQueryParam(req, 'saksnummer'); const url = saksnummer === null ? path : `${path}?saksnummer=${saksnummer}`; + const has_saksnummer = saksnummer !== null ? 'true' : 'false'; + const to_url = anonymizeSaksnummer(url); + const from_url = anonymizeSaksnummer(req.url); + log.warn({ msg: `Fixing path. Redirecting to ${url}`, data: { from_url, reason: 'deprecated' } }); + internalRedirectGauge.inc({ from_url, to_url, has_saksnummer }); + + res.cookie('redirected_from', from_url, { + maxAge: undefined, + expires: undefined, + httpOnly: true, + sameSite: 'strict', + }); + res.redirect(301, url); +}; - log.warn({ msg: `Fixing path. Redirecting to ${url}`, data: { url: req.url } }); +const getRedirectedFrom = (req: Request): string | undefined => { + const cookieHeader = req.header('cookie'); - res.redirect(301, url); + if (cookieHeader === undefined) { + return undefined; + } + + const { redirected_from } = parse(cookieHeader); + + if (redirected_from === undefined || redirected_from.length === 0) { + return undefined; + } + + return redirected_from; }; +const getHasSaksnummer = (req: Request) => (getQueryParam(req, 'saksnummer') !== null ? 'true' : 'false'); + +const anonymizeSaksnummer = (url: string) => url.replace(/(\?|&)saksnummer=[^&]+/, '$1saksnummer=SAKSNUMMER'); + const getInnsendingsytelseFromQueryParams = (req: Request) => getQueryParam(req, 'tittel') ?? getQueryParam(req, 'tema'); diff --git a/server/src/prometheus/middleware.ts b/server/src/prometheus/middleware.ts index cd1f1f89..bd349f02 100644 --- a/server/src/prometheus/middleware.ts +++ b/server/src/prometheus/middleware.ts @@ -1,4 +1,5 @@ import promBundle from 'express-prom-bundle'; +import { register } from 'prom-client'; import { NAIS_NAMESPACE } from '@app/config/env'; import { VERSION } from '@app/config/version'; import { normalizePath } from './normalize-path'; @@ -16,7 +17,7 @@ export const metricsMiddleware = promBundle({ excludeRoutes: ['/metrics', '/isAlive', '/isReady'], normalizePath: ({ originalUrl }) => normalizePath(originalUrl), customLabels: labels, - promClient: { collectDefaultMetrics: { labels } }, + promRegistry: register, formatStatusCode: ({ statusCode }) => { if (statusCode >= 200 && statusCode < 400) { return '2xx (3xx)'; diff --git a/server/src/prometheus/types.ts b/server/src/prometheus/types.ts new file mode 100644 index 00000000..ac49e8ea --- /dev/null +++ b/server/src/prometheus/types.ts @@ -0,0 +1,10 @@ +import { register } from 'prom-client'; +import { NAIS_NAMESPACE } from '@app/config/env'; +import { VERSION } from '@app/config/version'; + +register.setDefaultLabels({ + app_version: VERSION.substring(0, 7), + namespace: NAIS_NAMESPACE, +}); + +export const registers = [register]; diff --git a/server/src/routes/version.ts b/server/src/routes/version.ts deleted file mode 100644 index d42ce68b..00000000 --- a/server/src/routes/version.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Router } from 'express'; -import { VERSION } from '@app/config/version'; - -const router = Router(); - -const HEADERS = { - 'Content-Type': 'text/event-stream', - Connection: 'keep-alive', - 'Cache-Control': 'no-cache', -}; - -export const setupVersionRoute = () => { - router.get('/version', (_, res) => { - res.writeHead(200, HEADERS); - res.write('retry: 0\n'); - res.write(`data: ${VERSION}\n\n`); - }); - - return router; -}; diff --git a/server/src/types/http.ts b/server/src/types/http.ts index d1630e2a..639e227a 100644 --- a/server/src/types/http.ts +++ b/server/src/types/http.ts @@ -1,3 +1,5 @@ +import { CookieOptions } from 'express'; + export interface Request { readonly path: string; readonly method: string; @@ -8,4 +10,5 @@ export interface Request { export interface Response { readonly redirect: (status: number, path: string) => void; + readonly cookie: (name: string, value: string, options: CookieOptions) => void; }