Add minio support and forntend analitycs
This commit is contained in:
@@ -34,20 +34,47 @@ server {
|
||||
# Note: API routing is handled by ingress, not by this nginx
|
||||
# The frontend makes requests to /api which are routed by the ingress controller
|
||||
|
||||
# Static assets with aggressive caching (including source maps for debugging)
|
||||
location ~* ^/assets/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary Accept-Encoding;
|
||||
# Source map files - serve with proper CORS headers and content type
|
||||
# Note: These are typically only needed in development, but served in production for error reporting
|
||||
location ~* ^/assets/.*\.map$ {
|
||||
# Short cache time to avoid mismatches with JS files
|
||||
expires 1m;
|
||||
add_header Cache-Control "public, must-revalidate";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET";
|
||||
add_header Access-Control-Allow-Headers "Content-Type";
|
||||
add_header Content-Type "application/json";
|
||||
# Disable access logging for source maps as they're requested frequently
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Also handle JS and CSS files anywhere in the structure (for dynamic imports)
|
||||
location ~* \.(js|css)$ {
|
||||
# Static assets with appropriate caching
|
||||
# Note: JS/CSS files have content hashes for cache busting, but use shorter cache times to handle deployment issues
|
||||
location ~* ^/assets/.*\.(js|css)$ {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public";
|
||||
add_header Vary Accept-Encoding;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Static assets that don't change often (images, fonts) can have longer cache times
|
||||
location ~* ^/assets/.*\.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary Accept-Encoding;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Handle JS and CSS files anywhere in the structure (for dynamic imports) with shorter cache
|
||||
location ~* \.(js|css)$ {
|
||||
expires 1h;
|
||||
add_header Cache-Control "public";
|
||||
add_header Vary Accept-Encoding;
|
||||
access_log off;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
306
frontend/package-lock.json
generated
306
frontend/package-lock.json
generated
@@ -9,6 +9,13 @@
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.210.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
|
||||
"@opentelemetry/resources": "^2.4.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.4.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.4.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.39.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
@@ -2976,6 +2983,209 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api-logs": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.210.0.tgz",
|
||||
"integrity": "sha512-CMtLxp+lYDriveZejpBND/2TmadrrhUfChyxzmkFtHaMDdSKfP59MAYyA0ICBvEBdm3iXwLcaj/8Ic/pnGw9Yg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/core": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.4.0.tgz",
|
||||
"integrity": "sha512-KtcyFHssTn5ZgDu6SXmUznS80OFs/wN7y6MyFRRcKU6TOw8hNcGxKvt8hsdaLJfhzUszNSjURetq5Qpkad14Gw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-metrics-otlp-http": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.210.0.tgz",
|
||||
"integrity": "sha512-JpLThG8Hh8A/Jzdzw9i4Ftu+EzvLaX/LouN+mOOHmadL0iror0Qsi3QWzucXeiUsDDsiYgjfKyi09e6sltytgA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/otlp-exporter-base": "0.210.0",
|
||||
"@opentelemetry/otlp-transformer": "0.210.0",
|
||||
"@opentelemetry/resources": "2.4.0",
|
||||
"@opentelemetry/sdk-metrics": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-trace-otlp-http": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.210.0.tgz",
|
||||
"integrity": "sha512-9JkyaCl70anEtuKZdoCQmjDuz1/paEixY/DWfsvHt7PGKq3t8/nQ/6/xwxHjG+SkPAUbo1Iq4h7STe7Pk2bc5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/otlp-exporter-base": "0.210.0",
|
||||
"@opentelemetry/otlp-transformer": "0.210.0",
|
||||
"@opentelemetry/resources": "2.4.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.210.0.tgz",
|
||||
"integrity": "sha512-uk78DcZoBNHIm26h0oXc8Pizh4KDJ/y04N5k/UaI9J7xR7mL8QcMcYPQG9xxN7m8qotXOMDRW6qTAyptav4+3w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/otlp-transformer": "0.210.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.210.0.tgz",
|
||||
"integrity": "sha512-nkHBJVSJGOwkRZl+BFIr7gikA93/U8XkL2EWaiDbj3DVjmTEZQpegIKk0lT8oqQYfP8FC6zWNjuTfkaBVqa0ZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.210.0",
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/resources": "2.4.0",
|
||||
"@opentelemetry/sdk-logs": "0.210.0",
|
||||
"@opentelemetry/sdk-metrics": "2.4.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.4.0",
|
||||
"protobufjs": "8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.4.0.tgz",
|
||||
"integrity": "sha512-RWvGLj2lMDZd7M/5tjkI/2VHMpXebLgPKvBUd9LRasEWR2xAynDwEYZuLvY9P2NGG73HF07jbbgWX2C9oavcQg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs": {
|
||||
"version": "0.210.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.210.0.tgz",
|
||||
"integrity": "sha512-YuaL92Dpyk/Kc1o4e9XiaWWwiC0aBFN+4oy+6A9TP4UNJmRymPMEX10r6EMMFMD7V0hktiSig9cwWo59peeLCQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.210.0",
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/resources": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.4.0.tgz",
|
||||
"integrity": "sha512-qSbfq9mXbLMqmPEjijl32f3ZEmiHekebRggPdPjhHI6t1CsAQOR2Aw/SuTDftk3/l2aaPHpwP3xM2DkgBA1ANw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/resources": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.4.0.tgz",
|
||||
"integrity": "sha512-WH0xXkz/OHORDLKqaxcUZS0X+t1s7gGlumr2ebiEgNZQl2b0upK2cdoD0tatf7l8iP74woGJ/Kmxe82jdvcWRw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/resources": "2.4.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-web": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.4.0.tgz",
|
||||
"integrity": "sha512-1FYg7qnrgTugPev51SehxCp0v9J4P97MJn2MaXQ8QK//psfyLDorKAAC3LmSIhq7XaC726WSZ/Wm69r8NdjIsA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.4.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
|
||||
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -3010,6 +3220,70 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@@ -6577,7 +6851,6 @@
|
||||
"version": "20.19.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz",
|
||||
"integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@@ -11721,6 +11994,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -13119,6 +13398,30 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.0.0.tgz",
|
||||
"integrity": "sha512-jx6+sE9h/UryaCZhsJWbJtTEy47yXoGNYI4z8ZaRncM0zBKeRqjO2JEcOUYwrYGb1WLhXM1FfMzW3annvFv0rw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -15451,7 +15754,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unicode-canonical-property-names-ecmascript": {
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-http": "^0.210.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.210.0",
|
||||
"@opentelemetry/resources": "^2.4.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.4.0",
|
||||
"@opentelemetry/sdk-trace-web": "^2.4.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.39.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
|
||||
66
frontend/src/components/AnalyticsTestComponent.tsx
Normal file
66
frontend/src/components/AnalyticsTestComponent.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { trackUserAction, trackUserLocation } from '../utils/analytics';
|
||||
|
||||
const AnalyticsTestComponent: React.FC = () => {
|
||||
const [locationStatus, setLocationStatus] = useState<string>('Not requested');
|
||||
const [actionStatus, setActionStatus] = useState<string>('');
|
||||
|
||||
const handleTrackLocation = async () => {
|
||||
try {
|
||||
setLocationStatus('Requesting...');
|
||||
await trackUserLocation();
|
||||
setLocationStatus('Location tracked successfully!');
|
||||
} catch (error) {
|
||||
setLocationStatus('Error tracking location');
|
||||
console.error('Location tracking error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrackAction = () => {
|
||||
const actionName = `button_click_${Date.now()}`;
|
||||
trackUserAction(actionName, {
|
||||
component: 'AnalyticsTestComponent',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
setActionStatus(`Action "${actionName}" tracked`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-2xl mx-auto bg-white rounded-lg shadow-md">
|
||||
<h2 className="text-xl font-bold mb-4">Analytics Test Component</h2>
|
||||
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={handleTrackLocation}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4"
|
||||
>
|
||||
Track Location
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">{locationStatus}</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<button
|
||||
onClick={handleTrackAction}
|
||||
className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Track Action
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 ml-4">{actionStatus}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-gray-100 rounded">
|
||||
<h3 className="font-semibold mb-2">Expected Behavior:</h3>
|
||||
<ul className="list-disc pl-5 space-y-1 text-sm">
|
||||
<li>Page views are automatically tracked when this component loads</li>
|
||||
<li>Session information is captured on initial load</li>
|
||||
<li>Browser and device info is collected automatically</li>
|
||||
<li>Clicking buttons will generate user action traces</li>
|
||||
<li>Location tracking requires user permission</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsTestComponent;
|
||||
@@ -5,6 +5,9 @@ interface RuntimeConfig {
|
||||
VITE_API_URL: string;
|
||||
VITE_APP_TITLE: string;
|
||||
VITE_APP_VERSION: string;
|
||||
VITE_OTEL_TRACES_ENDPOINT?: string;
|
||||
VITE_OTEL_METRICS_ENDPOINT?: string;
|
||||
VITE_OTEL_ENABLED?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -27,6 +30,9 @@ function getRuntimeConfig(): RuntimeConfig {
|
||||
VITE_API_URL: import.meta.env.VITE_API_URL || 'http://localhost:8000',
|
||||
VITE_APP_TITLE: import.meta.env.VITE_APP_TITLE || 'PanIA Dashboard',
|
||||
VITE_APP_VERSION: import.meta.env.VITE_APP_VERSION || '1.0.0',
|
||||
VITE_OTEL_TRACES_ENDPOINT: import.meta.env.VITE_OTEL_TRACES_ENDPOINT || '/api/v1/telemetry/v1/traces',
|
||||
VITE_OTEL_METRICS_ENDPOINT: import.meta.env.VITE_OTEL_METRICS_ENDPOINT || '/api/v1/telemetry/v1/metrics',
|
||||
VITE_OTEL_ENABLED: import.meta.env.VITE_OTEL_ENABLED || 'true',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,6 +58,21 @@ export function isKubernetesEnvironment(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.__RUNTIME_CONFIG__;
|
||||
}
|
||||
|
||||
// Helper to check if OpenTelemetry is enabled
|
||||
export function isOpenTelemetryEnabled(): boolean {
|
||||
return config.VITE_OTEL_ENABLED?.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
// Helper to get OpenTelemetry traces endpoint
|
||||
export function getOtelTracesEndpoint(): string {
|
||||
return config.VITE_OTEL_TRACES_ENDPOINT || '/api/v1/telemetry/v1/traces';
|
||||
}
|
||||
|
||||
// Helper to get OpenTelemetry metrics endpoint
|
||||
export function getOtelMetricsEndpoint(): string {
|
||||
return config.VITE_OTEL_METRICS_ENDPOINT || '/api/v1/telemetry/v1/metrics';
|
||||
}
|
||||
|
||||
// Debug function to log current configuration
|
||||
export function logConfig(): void {
|
||||
console.log('Current configuration:', {
|
||||
|
||||
33
frontend/src/hooks/useAnalytics.ts
Normal file
33
frontend/src/hooks/useAnalytics.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
trackPageView,
|
||||
trackUserAction,
|
||||
trackUserLocation,
|
||||
trackSession,
|
||||
getCurrentUserId,
|
||||
isAnalyticsEnabled
|
||||
} from '../utils/analytics';
|
||||
|
||||
/**
|
||||
* React Hook for analytics
|
||||
*
|
||||
* NOTE: Page view tracking is handled globally by initializeAnalytics() in main.tsx.
|
||||
* This hook only exposes tracking functions for use in components.
|
||||
* Do NOT add automatic page tracking here to avoid duplicate events.
|
||||
*/
|
||||
export const useAnalytics = () => {
|
||||
return {
|
||||
// Manual page view tracking (use only for custom page events, not navigation)
|
||||
trackPageView,
|
||||
// Track user actions (button clicks, form submissions, etc.)
|
||||
trackUserAction,
|
||||
// Track user location (requires consent)
|
||||
trackUserLocation,
|
||||
// Track session (typically called once at app init)
|
||||
trackSession,
|
||||
// Get current user ID
|
||||
getCurrentUserId,
|
||||
// Check if analytics are enabled
|
||||
isAnalyticsEnabled
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,6 +7,92 @@ import './styles/animations.css';
|
||||
import './styles/themes/light.css';
|
||||
import './styles/themes/dark.css';
|
||||
|
||||
// OpenTelemetry Web SDK initialization
|
||||
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
|
||||
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
|
||||
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||
import { resourceFromAttributes } from '@opentelemetry/resources';
|
||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
|
||||
import { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
|
||||
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
|
||||
import { metrics } from '@opentelemetry/api';
|
||||
|
||||
// Import analytics utilities
|
||||
import { initializeAnalytics } from './utils/analytics';
|
||||
|
||||
// Import configuration
|
||||
import { isOpenTelemetryEnabled, getOtelTracesEndpoint, getOtelMetricsEndpoint } from './config/runtime';
|
||||
|
||||
// Store cleanup function for proper teardown
|
||||
let analyticsCleanup: (() => void) | null = null;
|
||||
|
||||
// Initialize OpenTelemetry
|
||||
const initOpenTelemetry = () => {
|
||||
// Check if OpenTelemetry is enabled in configuration
|
||||
if (!isOpenTelemetryEnabled()) {
|
||||
console.log('OpenTelemetry disabled by configuration');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Create resource with service information using non-deprecated attributes
|
||||
const resource = resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: 'bakery-frontend',
|
||||
[ATTR_SERVICE_VERSION]: '1.0.0'
|
||||
});
|
||||
|
||||
// Initialize tracer with span processor
|
||||
const traceExporter = new OTLPTraceExporter({
|
||||
url: getOtelTracesEndpoint() // Using configured endpoint
|
||||
});
|
||||
|
||||
const traceProvider = new WebTracerProvider({
|
||||
resource: resource,
|
||||
// Add span processors as array for current OpenTelemetry SDK version
|
||||
spanProcessors: [new BatchSpanProcessor(traceExporter)]
|
||||
});
|
||||
|
||||
traceProvider.register();
|
||||
|
||||
// Initialize metrics
|
||||
const metricExporter = new OTLPMetricExporter({
|
||||
url: getOtelMetricsEndpoint()
|
||||
});
|
||||
|
||||
const metricReader = new PeriodicExportingMetricReader({
|
||||
exporter: metricExporter,
|
||||
exportIntervalMillis: 10000, // 10 seconds
|
||||
});
|
||||
|
||||
// Use the MeterProvider constructor with readers array
|
||||
const meterProvider = new MeterProvider({
|
||||
resource: resource,
|
||||
readers: [metricReader]
|
||||
});
|
||||
|
||||
// Register the meter provider globally using proper API
|
||||
metrics.setGlobalMeterProvider(meterProvider);
|
||||
|
||||
console.log('OpenTelemetry initialized for frontend');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize OpenTelemetry:', error);
|
||||
// Continue without OpenTelemetry if initialization fails
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize OpenTelemetry before rendering the app
|
||||
initOpenTelemetry();
|
||||
|
||||
// Initialize analytics tracking and store cleanup function
|
||||
analyticsCleanup = initializeAnalytics();
|
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (analyticsCleanup) {
|
||||
analyticsCleanup();
|
||||
}
|
||||
});
|
||||
|
||||
// PWA/ServiceWorker functionality removed to avoid conflicts in development
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
|
||||
301
frontend/src/utils/analytics.ts
Normal file
301
frontend/src/utils/analytics.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import { trace } from '@opentelemetry/api';
|
||||
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
|
||||
|
||||
// Types and Interfaces
|
||||
interface AnalyticsMetadata {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
// Constants
|
||||
const ANALYTICS_ENABLED_KEY = 'analyticsEnabled';
|
||||
const LOCATION_CONSENT_KEY = 'locationTrackingConsent';
|
||||
const SESSION_ID_KEY = 'sessionId';
|
||||
const USER_ID_KEY = 'userId';
|
||||
|
||||
// Generate a unique session ID
|
||||
const generateSessionId = (): string => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substring(2);
|
||||
};
|
||||
|
||||
// Get current user ID (implement based on your auth system)
|
||||
export const getCurrentUserId = (): string | null => {
|
||||
// This is a placeholder - implement based on your authentication system
|
||||
// For example, you might get this from localStorage, cookies, or context
|
||||
return localStorage.getItem(USER_ID_KEY) || sessionStorage.getItem(USER_ID_KEY) || null;
|
||||
};
|
||||
|
||||
// Track page view
|
||||
export const trackPageView = (pathname: string): void => {
|
||||
// Check if analytics are enabled
|
||||
if (!isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tracer = trace.getTracer('bakery-frontend');
|
||||
const user_id = getCurrentUserId();
|
||||
|
||||
const span = tracer.startSpan('page_view', {
|
||||
attributes: {
|
||||
[ATTR_HTTP_ROUTE]: pathname,
|
||||
'user.id': user_id || 'anonymous',
|
||||
'page.path': pathname,
|
||||
}
|
||||
});
|
||||
|
||||
// End the span immediately for page views
|
||||
span.end();
|
||||
} catch (error) {
|
||||
console.error('Failed to track page view:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if analytics are enabled
|
||||
export const isAnalyticsEnabled = (): boolean => {
|
||||
return localStorage.getItem(ANALYTICS_ENABLED_KEY) !== 'false';
|
||||
};
|
||||
|
||||
// Enable or disable analytics
|
||||
export const setAnalyticsEnabled = (enabled: boolean): void => {
|
||||
localStorage.setItem(ANALYTICS_ENABLED_KEY, enabled.toString());
|
||||
};
|
||||
|
||||
// Check if location tracking consent is granted
|
||||
export const isLocationTrackingConsentGranted = (): boolean => {
|
||||
return localStorage.getItem(LOCATION_CONSENT_KEY) === 'granted';
|
||||
};
|
||||
|
||||
// Set location tracking consent
|
||||
export const setLocationTrackingConsent = (granted: boolean): void => {
|
||||
localStorage.setItem(LOCATION_CONSENT_KEY, granted ? 'granted' : 'denied');
|
||||
};
|
||||
|
||||
// Track user session
|
||||
export const trackSession = (): (() => void) => {
|
||||
// Check if analytics are enabled
|
||||
if (!isAnalyticsEnabled()) {
|
||||
console.log('Analytics disabled by user preference');
|
||||
return () => {}; // Return no-op cleanup function
|
||||
}
|
||||
|
||||
try {
|
||||
const tracer = trace.getTracer('bakery-frontend');
|
||||
const sessionId = generateSessionId();
|
||||
const userId = getCurrentUserId();
|
||||
|
||||
const span = tracer.startSpan('user_session', {
|
||||
attributes: {
|
||||
'session.id': sessionId,
|
||||
'user.id': userId || 'anonymous',
|
||||
'browser.user_agent': navigator.userAgent,
|
||||
'screen.width': window.screen.width.toString(),
|
||||
'screen.height': window.screen.height.toString(),
|
||||
'device.type': /mobile|tablet|ipad|iphone|ipod|android|silk/i.test(navigator.userAgent) ? 'mobile' : 'desktop'
|
||||
}
|
||||
});
|
||||
|
||||
// Store session ID in sessionStorage for later use
|
||||
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
||||
|
||||
// End span when session ends
|
||||
const handleBeforeUnload = () => {
|
||||
span.end();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Clean up event listener when needed
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to track session:', error);
|
||||
return () => {}; // Return no-op cleanup function
|
||||
}
|
||||
};
|
||||
|
||||
// Track user action
|
||||
export const trackUserAction = (action: string, metadata?: AnalyticsMetadata): void => {
|
||||
// Check if analytics are enabled
|
||||
if (!isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tracer = trace.getTracer('bakery-frontend');
|
||||
const userId = getCurrentUserId();
|
||||
|
||||
const span = tracer.startSpan('user_action', {
|
||||
attributes: {
|
||||
'user.action': action,
|
||||
'user.id': userId || 'anonymous',
|
||||
...metadata
|
||||
}
|
||||
});
|
||||
|
||||
span.end();
|
||||
} catch (error) {
|
||||
console.error('Failed to track user action:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Track user location (with consent)
|
||||
export const trackUserLocation = async (): Promise<void> => {
|
||||
// Check if analytics are enabled
|
||||
if (!isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if location tracking consent is granted
|
||||
if (!isLocationTrackingConsentGranted()) {
|
||||
console.log('Location tracking consent not granted');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const position = await new Promise<GeolocationPosition>((resolve, reject) => {
|
||||
if (!navigator.geolocation) {
|
||||
reject(new Error('Geolocation not supported'));
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 10000,
|
||||
maximumAge: 300000 // 5 minutes
|
||||
});
|
||||
});
|
||||
|
||||
const tracer = trace.getTracer('bakery-frontend');
|
||||
const userId = getCurrentUserId();
|
||||
|
||||
const span = tracer.startSpan('user_location', {
|
||||
attributes: {
|
||||
'user.id': userId || 'anonymous',
|
||||
'location.latitude': position.coords.latitude,
|
||||
'location.longitude': position.coords.longitude,
|
||||
'location.accuracy': position.coords.accuracy,
|
||||
'location.altitude': position.coords.altitude ?? undefined,
|
||||
'location.speed': position.coords.speed ?? undefined,
|
||||
'location.heading': position.coords.heading ?? undefined
|
||||
}
|
||||
});
|
||||
|
||||
span.end();
|
||||
} catch (error) {
|
||||
console.log('Location access denied or unavailable:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize analytics tracking
|
||||
export const initializeAnalytics = (): (() => void) => {
|
||||
// Track initial session
|
||||
const cleanupSession = trackSession();
|
||||
|
||||
// Track initial page view
|
||||
trackPageView(window.location.pathname);
|
||||
|
||||
// Listen for route changes (for SPA navigation)
|
||||
let previousUrl = window.location.href;
|
||||
|
||||
// For hash-based routing
|
||||
const handleHashChange = () => {
|
||||
if (window.location.href !== previousUrl) {
|
||||
trackPageView(window.location.pathname + window.location.search);
|
||||
previousUrl = window.location.href;
|
||||
}
|
||||
};
|
||||
|
||||
// For history API-based routing (most common in React apps)
|
||||
// Use proper typing for history state methods
|
||||
const originalPushState = history.pushState.bind(history);
|
||||
const handlePushState = function (
|
||||
this: History,
|
||||
data: unknown,
|
||||
unused: string,
|
||||
url?: string | URL | null
|
||||
) {
|
||||
originalPushState(data, unused, url);
|
||||
setTimeout(() => {
|
||||
if (window.location.href !== previousUrl) {
|
||||
trackPageView(window.location.pathname + window.location.search);
|
||||
previousUrl = window.location.href;
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const originalReplaceState = history.replaceState.bind(history);
|
||||
const handleReplaceState = function (
|
||||
this: History,
|
||||
data: unknown,
|
||||
unused: string,
|
||||
url?: string | URL | null
|
||||
) {
|
||||
originalReplaceState(data, unused, url);
|
||||
setTimeout(() => {
|
||||
if (window.location.href !== previousUrl) {
|
||||
trackPageView(window.location.pathname + window.location.search);
|
||||
previousUrl = window.location.href;
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
// Override history methods
|
||||
history.pushState = handlePushState;
|
||||
history.replaceState = handleReplaceState;
|
||||
|
||||
// Add event listeners
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
|
||||
// Track user consent for location if needed
|
||||
if (isLocationTrackingConsentGranted()) {
|
||||
trackUserLocation();
|
||||
}
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
// Restore original history methods
|
||||
history.pushState = originalPushState;
|
||||
history.replaceState = originalReplaceState;
|
||||
|
||||
// Remove event listeners
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
|
||||
// Clean up session tracking
|
||||
cleanupSession();
|
||||
};
|
||||
};
|
||||
|
||||
// Function to track custom metrics using OpenTelemetry spans
|
||||
export const trackCustomMetric = (
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: Record<string, string>
|
||||
): void => {
|
||||
// Check if analytics are enabled
|
||||
if (!isAnalyticsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Record metric as a span with the value as an attribute
|
||||
// This approach works well for browser-based metrics since
|
||||
// the OpenTelemetry metrics API in browsers sends to the same collector
|
||||
const tracer = trace.getTracer('bakery-frontend');
|
||||
const userId = getCurrentUserId();
|
||||
|
||||
const span = tracer.startSpan('custom_metric', {
|
||||
attributes: {
|
||||
'metric.name': name,
|
||||
'metric.value': value,
|
||||
'user.id': userId || 'anonymous',
|
||||
...attributes
|
||||
}
|
||||
});
|
||||
|
||||
span.end();
|
||||
} catch (error) {
|
||||
// Log error but don't fail - metrics are non-critical
|
||||
console.warn('Failed to track custom metric:', error);
|
||||
}
|
||||
};
|
||||
@@ -51,10 +51,11 @@ export default defineConfig(({ mode }) => {
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
// For production builds: ensure assets have correct paths
|
||||
// Base path should be '/' for root deployment
|
||||
// Base path should match the deployment URL
|
||||
base: process.env.VITE_BASE_URL || '/',
|
||||
// In development mode: inline source maps for better debugging
|
||||
// In production mode: external source maps
|
||||
sourcemap: isDevelopment ? 'inline' : true,
|
||||
// In production mode: external source maps (can be disabled with VITE_DISABLE_SOURCEMAPS)
|
||||
sourcemap: process.env.VITE_DISABLE_SOURCEMAPS ? false : (isDevelopment ? 'inline' : true),
|
||||
// In development mode: disable minification for readable errors
|
||||
// In production mode: use esbuild minification
|
||||
minify: isDevelopment ? false : 'esbuild',
|
||||
|
||||
Reference in New Issue
Block a user