mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
feat(perf): wire bundle analyzer and web-vitals beacon
This commit is contained in:
1 parent
b0ef80dcd4
commit
ebf83b1ee5
6 files changed
+274
-1
No files matched your search
+14
-1
@@ -1,4 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
import bundleAnalyzer from "@next/bundle-analyzer";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
@@ -24,4 +25,16 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
/**
|
||||
* v1.4.28 R3d — opt-in bundle analyzer behind `ANALYZE=1`.
|
||||
*
|
||||
* `pnpm analyze` (defined in `package.json`) sets the env var and
|
||||
* runs `next build`; the analyzer writes static HTML reports to
|
||||
* `.next/analyze/*.html`. The wrapper is a no-op when the env var is
|
||||
* unset so the regular `pnpm build` pipeline pays nothing.
|
||||
*/
|
||||
const withBundleAnalyzer = bundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === "1",
|
||||
});
|
||||
|
||||
export default withBundleAnalyzer(nextConfig);
|
||||
@@ -26,6 +26,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"analyze": "ANALYZE=1 next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"db:generate": "prisma generate",
|
||||
@@ -80,6 +81,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.11.3",
|
||||
"@next/bundle-analyzer": "^16.2.6",
|
||||
"@playwright/test": "^1.59.1",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testcontainers/postgresql": "^11.14.0",
|
||||
|
||||
Generated
+128
@@ -108,6 +108,9 @@ importers:
|
||||
'@axe-core/playwright':
|
||||
specifier: ^4.11.3
|
||||
version: 4.11.3(playwright-core@1.59.1)
|
||||
'@next/bundle-analyzer':
|
||||
specifier: ^16.2.6
|
||||
version: 16.2.6
|
||||
'@playwright/test':
|
||||
specifier: ^1.59.1
|
||||
version: 1.59.1
|
||||
@@ -497,6 +500,10 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7':
|
||||
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@dotenvx/dotenvx@1.65.0':
|
||||
resolution: {integrity: sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg==}
|
||||
hasBin: true
|
||||
@@ -1098,6 +1105,9 @@ packages:
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
'@next/bundle-analyzer@16.2.6':
|
||||
resolution: {integrity: sha512-amPkVtHCTJAdBwyhhl5+qztHk24O4JlASgrWqh15AmnYi74apfZR46NGC0u4pM6BMAU1mYld4WdzD3cRBP3dOA==}
|
||||
|
||||
'@next/env@16.2.6':
|
||||
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
|
||||
|
||||
@@ -1340,6 +1350,9 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@polka/url@1.0.0-next.29':
|
||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||
|
||||
'@prisma/adapter-pg@7.8.0':
|
||||
resolution: {integrity: sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==}
|
||||
|
||||
@@ -2951,6 +2964,10 @@ packages:
|
||||
peerDependencies:
|
||||
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -3331,6 +3348,10 @@ packages:
|
||||
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
commander@7.2.0:
|
||||
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
|
||||
engines: {node: '>= 14'}
|
||||
@@ -3481,6 +3502,9 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
debounce@1.2.1:
|
||||
resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@@ -3592,6 +3616,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
duplexer@0.1.2:
|
||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
@@ -4128,6 +4155,10 @@ packages:
|
||||
resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
gzip-size@6.0.0:
|
||||
resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4356,6 +4387,10 @@ packages:
|
||||
resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
@@ -4784,6 +4819,10 @@ packages:
|
||||
resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==}
|
||||
engines: {node: '>=10', npm: '>=6'}
|
||||
|
||||
mrmime@2.0.1:
|
||||
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -4953,6 +4992,10 @@ packages:
|
||||
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
opener@1.5.2:
|
||||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -5605,6 +5648,10 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sirv@2.0.4:
|
||||
resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
sisteransi@1.0.5:
|
||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||
|
||||
@@ -5847,6 +5894,10 @@ packages:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
totalist@3.0.1:
|
||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
|
||||
engines: {node: '>=16'}
|
||||
@@ -6113,6 +6164,11 @@ packages:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6159,6 +6215,18 @@ packages:
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
ws@7.5.10:
|
||||
resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==}
|
||||
engines: {node: '>=8.3.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
wsl-utils@0.3.1:
|
||||
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -6890,6 +6958,8 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@discoveryjs/json-ext@0.5.7': {}
|
||||
|
||||
'@dotenvx/dotenvx@1.65.0':
|
||||
dependencies:
|
||||
commander: 11.1.0
|
||||
@@ -7369,6 +7439,13 @@ snapshots:
|
||||
'@tybys/wasm-util': 0.10.1
|
||||
optional: true
|
||||
|
||||
'@next/bundle-analyzer@16.2.6':
|
||||
dependencies:
|
||||
webpack-bundle-analyzer: 4.10.1
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
'@next/env@16.2.6': {}
|
||||
|
||||
'@next/eslint-plugin-next@16.2.6':
|
||||
@@ -7607,6 +7684,8 @@ snapshots:
|
||||
dependencies:
|
||||
playwright: 1.59.1
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
|
||||
'@prisma/adapter-pg@7.8.0':
|
||||
dependencies:
|
||||
'@prisma/driver-adapter-utils': 7.8.0
|
||||
@@ -9378,6 +9457,10 @@ snapshots:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn-walk@8.3.5:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
@@ -9791,6 +9874,8 @@ snapshots:
|
||||
|
||||
commander@14.0.3: {}
|
||||
|
||||
commander@7.2.0: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
@@ -9928,6 +10013,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
debounce@1.2.1: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -10023,6 +10110,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
duplexer@0.1.2: {}
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
@@ -10760,6 +10849,10 @@ snapshots:
|
||||
|
||||
graphql@16.14.0: {}
|
||||
|
||||
gzip-size@6.0.0:
|
||||
dependencies:
|
||||
duplexer: 0.1.2
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
@@ -10958,6 +11051,8 @@ snapshots:
|
||||
|
||||
is-plain-obj@4.1.0: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
is-property@1.0.2: {}
|
||||
@@ -11329,6 +11424,8 @@ snapshots:
|
||||
|
||||
mmdb-lib@3.0.2: {}
|
||||
|
||||
mrmime@2.0.1: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msw@2.14.5(@types/node@25.6.2)(typescript@6.0.3):
|
||||
@@ -11520,6 +11617,8 @@ snapshots:
|
||||
powershell-utils: 0.1.0
|
||||
wsl-utils: 0.3.1
|
||||
|
||||
opener@1.5.2: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -12319,6 +12418,12 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sirv@2.0.4:
|
||||
dependencies:
|
||||
'@polka/url': 1.0.0-next.29
|
||||
mrmime: 2.0.1
|
||||
totalist: 3.0.1
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
||||
sonner@2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||
@@ -12610,6 +12715,8 @@ snapshots:
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
totalist@3.0.1: {}
|
||||
|
||||
tough-cookie@6.0.1:
|
||||
dependencies:
|
||||
tldts: 7.0.30
|
||||
@@ -12877,6 +12984,25 @@ snapshots:
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webpack-bundle-analyzer@4.10.1:
|
||||
dependencies:
|
||||
'@discoveryjs/json-ext': 0.5.7
|
||||
acorn: 8.16.0
|
||||
acorn-walk: 8.3.5
|
||||
commander: 7.2.0
|
||||
debounce: 1.2.1
|
||||
escape-string-regexp: 4.0.0
|
||||
gzip-size: 6.0.0
|
||||
html-escaper: 2.0.2
|
||||
is-plain-object: 5.0.0
|
||||
opener: 1.5.2
|
||||
picocolors: 1.1.1
|
||||
sirv: 2.0.4
|
||||
ws: 7.5.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
which-boxed-primitive@1.1.1:
|
||||
dependencies:
|
||||
is-bigint: 1.1.0
|
||||
@@ -12947,6 +13073,8 @@ snapshots:
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
ws@7.5.10: {}
|
||||
|
||||
wsl-utils@0.3.1:
|
||||
dependencies:
|
||||
is-wsl: 3.1.1
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* v1.4.28 R3d — Web Vitals beacon.
|
||||
*
|
||||
* Next 16 exposes `reportWebVitals` from the root layout; we wire the
|
||||
* client to POST every measurement to this internal route, which logs
|
||||
* to the shared wide-event pipeline so ops can see CLS / FCP / FID /
|
||||
* LCP / TTFB / INP histograms in the same place the rest of the
|
||||
* request flow surfaces.
|
||||
*
|
||||
* Why a route + not the front-end alone:
|
||||
*
|
||||
* - `console.log` from the client is invisible to the server-side
|
||||
* observability stack (HealthLog ships logs through the
|
||||
* `WideEventBuilder` in `src/lib/logging/context.ts`). Funnelling
|
||||
* metrics through a thin route reuses the existing pipeline.
|
||||
* - Persisting elsewhere (Postgres, S3) would be a wasted spend —
|
||||
* this is a beacon. We log and forget.
|
||||
*
|
||||
* Authentication: none (the route is internal-only by name; CSP +
|
||||
* same-origin policy keep cross-site reporting out). The dispatch is
|
||||
* idempotent — a duplicate beacon is at worst one extra log line.
|
||||
*
|
||||
* Rate-limit: deliberately none. Web Vitals emit one report per metric
|
||||
* per page load (max 6 per session). A bot trying to flood the route
|
||||
* is cheaper to drop at the edge.
|
||||
*
|
||||
* Payload shape: matches `web-vitals`' `Metric` interface — `id`,
|
||||
* `name`, `value`, `delta`, `rating`. We log the lot; if a future
|
||||
* client adds fields we'll see them in the event meta.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { apiHandler } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
|
||||
interface WebVitalsBody {
|
||||
id?: unknown;
|
||||
name?: unknown;
|
||||
value?: unknown;
|
||||
delta?: unknown;
|
||||
rating?: unknown;
|
||||
navigationType?: unknown;
|
||||
}
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
let body: WebVitalsBody | null = null;
|
||||
try {
|
||||
body = (await request.json()) as WebVitalsBody;
|
||||
} catch {
|
||||
// Malformed payload — accept silently so the client doesn't retry.
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
if (!body || typeof body !== "object") {
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
const name = typeof body.name === "string" ? body.name : "unknown";
|
||||
const value =
|
||||
typeof body.value === "number" && Number.isFinite(body.value)
|
||||
? body.value
|
||||
: null;
|
||||
const rating = typeof body.rating === "string" ? body.rating : null;
|
||||
|
||||
annotate({
|
||||
meta: {
|
||||
"web_vitals.name": name,
|
||||
"web_vitals.value": value,
|
||||
"web_vitals.rating": rating,
|
||||
"web_vitals.navigation_type":
|
||||
typeof body.navigationType === "string" ? body.navigationType : null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import "./globals.css";
|
||||
import { Providers } from "@/components/providers";
|
||||
import { AuthShell } from "@/components/layout/auth-shell";
|
||||
import { MonitoringBootstrap } from "@/components/monitoring/bootstrap";
|
||||
import { WebVitalsReporter } from "@/components/monitoring/web-vitals-reporter";
|
||||
import { parseLocaleFromAcceptLanguage } from "@/lib/format-locale";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
@@ -96,6 +97,7 @@ export default async function RootLayout({
|
||||
<body className={`${inter.variable} font-sans antialiased`}>
|
||||
<Providers initialLocale={initialLocale}>
|
||||
<MonitoringBootstrap />
|
||||
<WebVitalsReporter />
|
||||
<AuthShell>{children}</AuthShell>
|
||||
</Providers>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { useReportWebVitals } from "next/web-vitals";
|
||||
|
||||
/**
|
||||
* v1.4.28 R3d — client-side mount that pipes every Web Vitals
|
||||
* measurement to `/api/internal/web-vitals`.
|
||||
*
|
||||
* Next 16 ships `useReportWebVitals` as the supported hook for
|
||||
* capturing CLS / FCP / FID / LCP / TTFB / INP from the live
|
||||
* navigation. The hook fires once per metric per page load, so even a
|
||||
* heavy navigation produces at most six beacons.
|
||||
*
|
||||
* Delivery transport: `navigator.sendBeacon` when available (queues
|
||||
* the POST without blocking the page lifecycle, survives the
|
||||
* `pagehide` event) with a `fetch({ keepalive: true })` fallback.
|
||||
* The route accepts the payload best-effort and returns 200 even on
|
||||
* malformed bodies — beacons must never delay or fail the navigation.
|
||||
*/
|
||||
export function WebVitalsReporter() {
|
||||
useReportWebVitals((metric) => {
|
||||
const body = JSON.stringify({
|
||||
id: metric.id,
|
||||
name: metric.name,
|
||||
value: metric.value,
|
||||
delta: metric.delta,
|
||||
rating: metric.rating,
|
||||
navigationType: metric.navigationType,
|
||||
});
|
||||
const url = "/api/internal/web-vitals";
|
||||
|
||||
if (
|
||||
typeof navigator !== "undefined" &&
|
||||
typeof navigator.sendBeacon === "function"
|
||||
) {
|
||||
navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback for non-`sendBeacon` browsers — `keepalive` keeps the
|
||||
// request alive past navigation. We deliberately ignore the
|
||||
// response (it's a beacon, not a transaction).
|
||||
void fetch(url, {
|
||||
method: "POST",
|
||||
body,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
keepalive: true,
|
||||
}).catch(() => undefined);
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in new issue
Block a user