From 5a2b9e1319e3e26d8a5f56c2ffe7fce8e4b94191 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Thu, 7 May 2026 22:59:30 +0300 Subject: [PATCH 01/23] Chore: Gitignore .env.development and add example template - Automated .env.development creation using a script if missing. - Updated gitignore to avoid committing local env files. - Facilitates smoother local development without manual setup. --- .env.development => .env.development.example | 7 +++++++ .gitignore | 3 +++ scripts/configure-network.sh | 13 +++++++++++++ 3 files changed, 23 insertions(+) rename .env.development => .env.development.example (53%) diff --git a/.env.development b/.env.development.example similarity index 53% rename from .env.development rename to .env.development.example index 072c8714..432d3619 100644 --- a/.env.development +++ b/.env.development.example @@ -1,4 +1,11 @@ # Development Environment Variables +# +# Copy this file to .env.development and adjust values for your machine: +# cp .env.development.example .env.development +# +# Then run ./scripts/configure-network.sh to set your local IP automatically. +# Do NOT commit .env.development — it is gitignored. + CERT_PATH=./certs CONTAINER_USER=nobody SERVER_ENV=docker-dev diff --git a/.gitignore b/.gitignore index d706cfc6..2570caf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .DS_Store .env +.env.development +.env.production +.env.*.local target/* node_modules/* certs/* diff --git a/scripts/configure-network.sh b/scripts/configure-network.sh index e3874de0..e509b85b 100755 --- a/scripts/configure-network.sh +++ b/scripts/configure-network.sh @@ -56,6 +56,19 @@ update_file() { echo "Updated $file" } +# Bootstrap .env.development from the example template if missing. +# .env.development is gitignored, so a fresh checkout won't have it. +ENV_FILE="$PROJECT_ROOT/.env.development" +ENV_EXAMPLE="$PROJECT_ROOT/.env.development.example" +if [ ! -f "$ENV_FILE" ]; then + if [ ! -f "$ENV_EXAMPLE" ]; then + echo "Error: $ENV_EXAMPLE not found" + exit 1 + fi + cp "$ENV_EXAMPLE" "$ENV_FILE" + echo "Created $ENV_FILE from $ENV_EXAMPLE" +fi + # Get local IP address echo "Please enter your local IP address (e.g., 192.168.1.100):" read -r local_ip From 092c278bb98cd3722effe80cf1e0f22bd2db44c9 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Thu, 7 May 2026 23:42:04 +0300 Subject: [PATCH 02/23] Chore: Add .env.production template and require explicit env vars - New .env.production.example template (gitignored real file lives on host) - Makefile prod target requires .env.production - docker-compose.yml: ${VAR:?error} replaces silent defaults for required vars - .env.development.example: add SENTRY_ENABLED / SENTRY_DSN placeholders --- .env.development.example | 7 +++++++ .env.production.example | 28 ++++++++++++++++++++++++++++ Makefile | 5 +++-- docker-compose.yml | 26 ++++++++++++++------------ 4 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 .env.production.example diff --git a/.env.development.example b/.env.development.example index 432d3619..5d838f5d 100644 --- a/.env.development.example +++ b/.env.development.example @@ -19,3 +19,10 @@ RUST_BUILD_MODE=debug HOST=127.0.0.1 PORT=4000 SERVER_NAME=127.0.0.1 + +# Sentry (error tracking) +# Leave SENTRY_ENABLED=false in dev unless you actively want to send events. +# SENTRY_DSN must stay empty in this committed file — set it only in your +# local .env.development (which is gitignored). +SENTRY_ENABLED=false +SENTRY_DSN= diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 00000000..5e41c6af --- /dev/null +++ b/.env.production.example @@ -0,0 +1,28 @@ +# Production Environment Variables +# +# This file is a template. On your production host (e.g. the EC2 instance): +# cp .env.production.example .env.production +# chmod 600 .env.production +# # then edit .env.production and fill in real secrets (SENTRY_DSN, etc.) +# +# .env.production is gitignored — it must NEVER be committed. + +CERT_PATH=/etc/ssl/pastepoint +CONTAINER_USER=nobody +SERVER_ENV=production + +# Build Configuration for Production +BUILD_MODE=production +NPM_BUILD_CONFIG=docker +RUST_BUILD_MODE=release + +# NGINX Configuration for Production +HOST=0.0.0.0 +PORT=4000 +SERVER_NAME=pastepoint.com + +# Sentry (error tracking) +# Set SENTRY_DSN to the production server DSN. Without it, Sentry stays +# disabled even if SENTRY_ENABLED=true. +SENTRY_ENABLED=true +SENTRY_DSN= diff --git a/Makefile b/Makefile index f31d76a7..c65bc43f 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,9 @@ export COMPOSE_DOCKER_CLI_BUILD=1 # Production environment (default) prod: @echo "Starting production environment..." - docker compose build --parallel - docker compose up --force-recreate -d + @test -f .env.production || (echo "Error: .env.production not found. Copy .env.production.example to .env.production on this host and fill in real values." && exit 1) + docker compose --env-file .env.production build --parallel + docker compose --env-file .env.production up --force-recreate -d @echo "Production services are starting. View logs with: make logs" # Development environment diff --git a/docker-compose.yml b/docker-compose.yml index c07f81fa..37828cb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,13 @@ services: # ---------- Certificate checker ---------- cert-checker: image: alpine - user: ${CONTAINER_USER:-nobody} + user: ${CONTAINER_USER:?CONTAINER_USER is required (set it in your .env file)} container_name: pastepoint-cert-checker entrypoint: | sh -c ' if [ ! -f /etc/ssl/pastepoint/cert.pem ] || [ ! -f /etc/ssl/pastepoint/key.pem ]; then echo "ERROR: Missing SSL certificates!" - echo "Please create ${CERT_PATH:-/etc/ssl/pastepoint} directory with cert.pem and key.pem files" + echo "Please create ${CERT_PATH} directory with cert.pem and key.pem files" echo "Use generate-certs.sh script to generate self-signed certificates for development or provide your own for production." echo "Exiting..." exit 1 @@ -16,7 +16,7 @@ services: echo "Certificates exist, continuing..." ' volumes: - - ${CERT_PATH:-/etc/ssl/pastepoint}:/etc/ssl/pastepoint:ro + - ${CERT_PATH:?CERT_PATH is required (set it in your .env file)}:/etc/ssl/pastepoint:ro restart: 'no' # ---------- Server container ---------- @@ -25,14 +25,16 @@ services: context: . dockerfile: server/Dockerfile args: - RUST_BUILD_MODE: ${RUST_BUILD_MODE:-release} + RUST_BUILD_MODE: ${RUST_BUILD_MODE:?RUST_BUILD_MODE is required (set it in your .env file)} container_name: pastepoint-server expose: - '9000' environment: - - RUN_ENV=${SERVER_ENV:-production} + - RUN_ENV=${SERVER_ENV:?SERVER_ENV is required (set it in your .env file)} + - SENTRY_ENABLED=${SENTRY_ENABLED:-false} + - SENTRY_DSN=${SENTRY_DSN:-} volumes: - - ${CERT_PATH:-/etc/ssl/pastepoint}:/etc/ssl/pastepoint:ro + - ${CERT_PATH}:/etc/ssl/pastepoint:ro depends_on: cert-checker: condition: service_completed_successfully @@ -52,11 +54,11 @@ services: dockerfile: client/web/Dockerfile target: ssr args: - NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG:-docker} + NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG:?NPM_BUILD_CONFIG is required (set it in your .env file)} container_name: pastepoint-ssr environment: - PORT: ${PORT:-4000} - HOST: ${HOST:-0.0.0.0} + PORT: ${PORT:?PORT is required (set it in your .env file)} + HOST: ${HOST:?HOST is required (set it in your .env file)} expose: - '4000' depends_on: @@ -78,17 +80,17 @@ services: dockerfile: client/web/Dockerfile target: nginx args: - NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG:-docker} + NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG} container_name: pastepoint-nginx environment: - SERVER_NAME: ${SERVER_NAME:-pastepoint.com} + SERVER_NAME: ${SERVER_NAME:?SERVER_NAME is required (set it in your .env file)} SSL_CERT_PATH: /etc/ssl/pastepoint/cert.pem SSL_CERT_KEY_PATH: /etc/ssl/pastepoint/key.pem ports: - '80:80' - '443:443' volumes: - - ${CERT_PATH:-/etc/ssl/pastepoint}:/etc/ssl/pastepoint:ro + - ${CERT_PATH}:/etc/ssl/pastepoint:ro depends_on: ssr: condition: service_healthy From 0ff1822c26668034d75d7c1ab92f9ae5909ed213 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Thu, 7 May 2026 23:47:55 +0300 Subject: [PATCH 03/23] Server: Integrate Sentry for error reporting - Add sentry + sentry-actix crates with privacy-tight defaults (send_default_pii=false, before_send strips user/headers/body/query, loopback IP suppresses geo-IP enrichment server-side) - New SentryConfig loaded from TOML; SENTRY_ENABLED / SENTRY_DSN env vars override and gate initialization - Quiet noisy transport crates (reqwest/hyper/h2/rustls) in env_logger filter so app logs stay readable --- server/Cargo.lock | 1082 ++++++++++++++++++++++++++++++-- server/Cargo.toml | 2 + server/config/development.toml | 7 + server/config/docker-dev.toml | 7 + server/config/production.toml | 7 + server/src/config.rs | 41 ++ server/src/lib.rs | 2 +- server/src/main.rs | 67 +- 8 files changed, 1166 insertions(+), 49 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index 03debaac..c3a45533 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "flate2", "foldhash 0.1.5", "futures-core", - "h2", + "h2 0.3.27", "http 0.2.12", "httparse", "httpdate", @@ -349,6 +349,15 @@ dependencies = [ "syn", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -397,6 +406,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -470,6 +488,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "awc" version = "3.8.2" @@ -489,7 +513,7 @@ dependencies = [ "derive_more", "futures-core", "futures-util", - "h2", + "h2 0.3.27", "http 0.2.12", "itoa", "log", @@ -503,12 +527,33 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.11.1" @@ -536,6 +581,15 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "brotli" version = "8.0.2" @@ -596,6 +650,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.10.0" @@ -688,6 +748,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -769,6 +845,26 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -828,6 +924,16 @@ dependencies = [ "crypto-common 0.2.1", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -924,12 +1030,30 @@ dependencies = [ "rand 0.10.1", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "flate2" version = "1.1.9" @@ -1125,6 +1249,12 @@ dependencies = [ "wasip3", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "governor" version = "0.10.4" @@ -1167,6 +1297,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1214,6 +1363,23 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd" +dependencies = [ + "cfg-if", + "libc", + "windows-link", +] + [[package]] name = "http" version = "0.2.12" @@ -1235,6 +1401,29 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -1256,6 +1445,81 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.14", + "http 1.4.0", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1383,6 +1647,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1458,6 +1728,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1470,6 +1746,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1542,6 +1824,35 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nonzero_ext" version = "0.3.0" @@ -1555,82 +1866,272 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] -name = "once_cell" -version = "1.21.4" +name = "objc2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "objc2-cloud-kit" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags", + "objc2", + "objc2-foundation", +] [[package]] -name = "openssl" -version = "0.10.79" +name = "objc2-core-data" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", + "objc2", + "objc2-foundation", ] [[package]] -name = "openssl-macros" -version = "0.1.1" +name = "objc2-core-foundation" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bitflags", + "dispatch2", + "objc2", ] [[package]] -name = "openssl-sys" -version = "0.9.115" +name = "objc2-core-graphics" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "objc2-core-image" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "objc2", + "objc2-foundation", ] [[package]] -name = "parking_lot" -version = "0.12.5" +name = "objc2-core-location" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "lock_api", - "parking_lot_core", + "objc2", + "objc2-foundation", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "objc2-core-text" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "cfg-if", - "libc", + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", "redox_syscall", "smallvec", "windows-link", @@ -1642,6 +2143,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1896,6 +2406,59 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.14", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.12.1" @@ -1920,6 +2483,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1929,6 +2498,52 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1941,18 +2556,172 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +[[package]] +name = "sentry" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93b3e19f45495ddd41d8222a152c48c84f6ba45abe9c69e2527e9cdea29bb5b" +dependencies = [ + "cfg_aliases", + "httpdate", + "native-tls", + "reqwest", + "sentry-actix", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-debug-images", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq", +] + +[[package]] +name = "sentry-actix" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168d0312e1b1741d8295a16c7b2c62c10c76302f7476a1749d6ccc14cb40663a" +dependencies = [ + "actix-http", + "actix-web", + "bytes", + "futures-util", + "sentry-core", +] + +[[package]] +name = "sentry-backtrace" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84c325ace9ca2388e510fe7d6672b5d60cd8b3bd0eb4bb4ee8314c323cd686" +dependencies = [ + "backtrace", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896c1ab62dbfe1746fb262bbf72e6feb2fb9dfb2c14709077bf71beb532e44b2" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f5abf20c42cb1593ec1638976e2647da55f79bccac956444c1707b6cce259a" +dependencies = [ + "rand 0.9.4", + "sentry-types", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "sentry-debug-images" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b88bbe6a760d5724bb40689827e82e8db1e275947df2c59abe171bfc30bb671" +dependencies = [ + "findshlibs", + "sentry-core", +] + +[[package]] +name = "sentry-panic" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0260dcb52562b6a79ae7702312a26dba94b79fb5baee7301087529e5ca4e872e" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1c035f3a0a8671ae1a231c5b457abb68b71acba2bf3054dab2a09a9d4ea487e" +dependencies = [ + "bitflags", + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d8e81058ec155992191f61c7b29bfa7b2cf12012131e7cdc0678020898a7c9" +dependencies = [ + "debugid", + "hex", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror", + "time", + "url", + "uuid", +] + [[package]] name = "serde" version = "1.0.228" @@ -2052,6 +2821,8 @@ dependencies = [ "log", "openssl", "rand 0.10.1", + "sentry", + "sentry-actix", "serde", "serde_json", "tokio", @@ -2150,6 +2921,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -2161,6 +2938,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2172,6 +2958,39 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.47" @@ -2238,6 +3057,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-openssl" version = "0.6.5" @@ -2249,6 +3078,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -2293,6 +3132,51 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -2323,8 +3207,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "tracing-core", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typeid" version = "1.0.3" @@ -2343,6 +3243,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + [[package]] name = "unicode-ident" version = "1.0.24" @@ -2361,6 +3270,41 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64", + "http 1.4.0", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -2371,8 +3315,15 @@ dependencies = [ "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2393,9 +3344,16 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", + "serde_core", "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2408,6 +3366,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2445,6 +3412,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.120" @@ -2531,6 +3508,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2825,6 +3811,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/server/Cargo.toml b/server/Cargo.toml index 33d08b96..e4ecae79 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,6 +31,8 @@ rand = "0.10.1" bytes = "1.11.1" config = "0.15.22" url = "2.5.8" +sentry = "0.48" +sentry-actix = "0.48" [dev-dependencies] actix-test = "0.1.5" diff --git a/server/config/development.toml b/server/config/development.toml index dc05e6e4..0b5f9c7f 100644 --- a/server/config/development.toml +++ b/server/config/development.toml @@ -7,3 +7,10 @@ rate_limit_per_second = 100 rate_limit_burst_size = 200 log_level = "debug" cors_allowed_origins = "https://127.0.0.1" + +[sentry] +# DSN is read from the SENTRY_DSN env var, never committed. +# SENTRY_ENABLED env var overrides `enabled` below. +enabled = false +environment = "development" +sample_rate = 1.0 diff --git a/server/config/docker-dev.toml b/server/config/docker-dev.toml index c7430670..86858209 100644 --- a/server/config/docker-dev.toml +++ b/server/config/docker-dev.toml @@ -7,3 +7,10 @@ rate_limit_per_second = 100 rate_limit_burst_size = 200 log_level = "debug" cors_allowed_origins = "https://127.0.0.1" + +[sentry] +# DSN is read from the SENTRY_DSN env var, never committed. +# SENTRY_ENABLED env var overrides `enabled` below. +enabled = false +environment = "docker-dev" +sample_rate = 1.0 diff --git a/server/config/production.toml b/server/config/production.toml index 2f26ce87..c860aae2 100644 --- a/server/config/production.toml +++ b/server/config/production.toml @@ -7,3 +7,10 @@ rate_limit_per_second = 50 rate_limit_burst_size = 100 log_level = "info" cors_allowed_origins = "https://pastepoint.com" + +[sentry] +# DSN is read from the SENTRY_DSN env var, never committed. +# SENTRY_ENABLED env var overrides `enabled` below. +enabled = true +environment = "production" +sample_rate = 1.0 diff --git a/server/src/config.rs b/server/src/config.rs index c67ac8e7..c77ff9e9 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -9,6 +9,47 @@ fn default_log_level() -> String { "debug".to_string() } +fn default_sentry_sample_rate() -> f32 { + 1.0 +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SentryConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default)] + pub environment: Option, + #[serde(default = "default_sentry_sample_rate")] + pub sample_rate: f32, +} + +impl Default for SentryConfig { + fn default() -> Self { + Self { + enabled: false, + environment: None, + sample_rate: default_sentry_sample_rate(), + } + } +} + +impl SentryConfig { + pub fn load() -> Self { + let environment = env::var("RUN_ENV").unwrap_or_else(|_| "development".to_string()); + + let mut cfg: Self = Config::builder() + .add_source(File::with_name(&format!("config/{environment}")).required(false)) + .build() + .and_then(|s| s.get::("sentry")) + .unwrap_or_default(); + + if let Ok(v) = env::var("SENTRY_ENABLED") { + cfg.enabled = matches!(v.to_ascii_lowercase().as_str(), "true" | "1" | "yes"); + } + cfg + } +} + #[derive(Clone, Debug, Deserialize)] pub struct ServerConfig { pub bind_address: String, diff --git a/server/src/lib.rs b/server/src/lib.rs index dc19abe2..bc039da5 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -9,7 +9,7 @@ mod server; mod session; mod session_store; -pub use config::ServerConfig; +pub use config::{SentryConfig, ServerConfig}; pub use consts::{ CLEANUP_INTERVAL, CONTENT_TYPE_TEXT_PLAIN, CORS_MAX_AGE, HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, KEEP_ALIVE_INTERVAL, MAX_FRAME_SIZE, MAX_SIGNAL_SIZE, MIN_USER_AGENT_LENGTH, SAFE_CHARSET, diff --git a/server/src/main.rs b/server/src/main.rs index be12a4f3..de65f63d 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,20 +4,80 @@ use actix_http::KeepAlive; use actix_web::{App, HttpServer, middleware::Logger, web::Data}; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use server::{ - CORS_MAX_AGE, KEEP_ALIVE_INTERVAL, ServerConfig, SessionStore, chat_ws, create_session, health, - index, private_chat_ws, + CORS_MAX_AGE, KEEP_ALIVE_INTERVAL, SentryConfig, ServerConfig, SessionStore, chat_ws, + create_session, health, index, private_chat_ws, }; +use std::borrow::Cow; use std::io::Result; +use std::net::{IpAddr, Ipv4Addr}; +use std::sync::Arc; const VERSION: &str = env!("CARGO_PKG_VERSION"); const NAME: &str = env!("CARGO_PKG_NAME"); const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); +fn init_sentry(cfg: &SentryConfig) -> Option { + if !cfg.enabled { + return None; + } + let dsn = std::env::var("SENTRY_DSN").ok().filter(|s| !s.is_empty())?; + + let options = sentry::ClientOptions { + release: sentry::release_name!(), + environment: cfg.environment.clone().map(Cow::Owned), + sample_rate: cfg.sample_rate, + send_default_pii: false, + attach_stacktrace: true, + max_breadcrumbs: 50, + before_send: Some(Arc::new(|mut event| { + // Strip anything that could carry user-identifying data before the + // event leaves the process. + event.user = Some(sentry::protocol::User { + ip_address: Some(sentry::protocol::IpAddress::Exact(IpAddr::V4( + Ipv4Addr::LOCALHOST, + ))), + ..Default::default() + }); + event.server_name = None; + if let Some(req) = event.request.as_mut() { + req.cookies = None; + req.headers.clear(); + req.data = None; + req.query_string = None; + } + log::info!( + target: "Sentry", + "Captured event {} level={:?} message={:?}", + event.event_id, + event.level, + event.message.as_deref().unwrap_or("") + ); + Some(event) + })), + ..Default::default() + }; + + Some(sentry::init((dsn, options))) +} + #[actix_web::main] async fn main() -> Result<()> { + let sentry_cfg = SentryConfig::load(); + let _sentry_guard = init_sentry(&sentry_cfg); + let config = ServerConfig::load(None).expect("Failed to load server configuration"); - env_logger::init_from_env(env_logger::Env::new().default_filter_or(&config.log_level)); + let log_filter = format!( + "{lvl},reqwest=warn,hyper=warn,hyper_util=warn,h2=warn,rustls=warn,tokio_util=warn", + lvl = config.log_level + ); + env_logger::init_from_env(env_logger::Env::new().default_filter_or(log_filter)); + + if _sentry_guard.is_some() { + log::info!(target: "Websocket", "Sentry error reporting enabled"); + } else { + log::debug!(target: "Websocket", "Sentry error reporting disabled"); + } let governor_conf = GovernorConfigBuilder::default() .requests_per_second(config.rate_limit_per_second) .burst_size(config.rate_limit_burst_size) @@ -65,6 +125,7 @@ async fn main() -> Result<()> { .wrap(Governor::new(&governor_conf)) .wrap(Logger::default()) .wrap(cors) + .wrap(sentry_actix::Sentry::new()) .app_data(session_manager.clone()) .app_data(server_config_for_app) .service(index) From 5c8ac453291b331cb92477cd13f26d50a9014076 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 00:45:09 +0300 Subject: [PATCH 04/23] Web: Integrate Sentry for error reporting - Add @sentry/angular ^10.52.0 - Init in main.ts before bootstrap, gated by environment.sentry.enabled + DSN - Sentry.createErrorHandler registered as Angular ErrorHandler - Privacy-tight beforeSend: strips user/headers/body/query/cookies, device timezone/locale, and culture context; forces 127.0.0.1 IP to suppress server-side geo enrichment - Replay and Performance disabled (no integrations added, sample rates 0) --- client/web/package-lock.json | 96 +++++++++++++++++++ client/web/package.json | 1 + client/web/src/app/app.config.ts | 6 ++ .../environments/environment.docker-dev.ts | 5 + .../web/src/environments/environment.prod.ts | 5 + client/web/src/environments/environment.ts | 5 + client/web/src/main.ts | 36 +++++++ 7 files changed, 154 insertions(+) diff --git a/client/web/package-lock.json b/client/web/package-lock.json index 46b63d00..9fa16930 100644 --- a/client/web/package-lock.json +++ b/client/web/package-lock.json @@ -21,6 +21,7 @@ "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@ngxpert/hot-toast": "^6.1.0", + "@sentry/angular": "^10.52.0", "async-mutex": "^0.5.0", "autolinker": "^4.1.5", "compression": "^1.8.1", @@ -6807,6 +6808,101 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@sentry-internal/browser-utils": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.52.0.tgz", + "integrity": "sha512-x/yEPZdpH6NGQeoeQnV9tj8reAH8twNttiltGZl2o8Rk7sQeUfe7E8yuYP2XbJ2RqyZK5qRS3COrNyMPzf6KFA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.52.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.52.0.tgz", + "integrity": "sha512-5kAn1W8ZvCuHtEHXpq6iRkUMdNCilwww+YxaN2yofVrCivAbB3Ha5JJUMqmWOPW0pC27zGYmoJMIDvG+PczUxA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "10.52.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.52.0.tgz", + "integrity": "sha512-diywyuc/H7VTUR+W5ryVmLF+0X4UP1OskMqb6V8RSAvJHcj2JmIm7uP+Fc6ACTno+b6AUShwT/L4xVXzO6X9Cw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.52.0", + "@sentry/core": "10.52.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.52.0.tgz", + "integrity": "sha512-BI5ie4dxPuUJ344CXVSnAxY1xZCbghglPSCIlTOYODpR9so9yo5IZh+Mwspt0oWsUMaxWJiQSNYlbPWi7WDavg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "10.52.0", + "@sentry/core": "10.52.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/angular": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-10.52.0.tgz", + "integrity": "sha512-IT0m1b0fJHyHGjl+P1h99Fmh7PHTu4zqKGRNthEKTmkUCsEOdTxxx0nYFQU7Elfl8ZWsnCQ8rhJjr9SyJ1JX6g==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "10.52.0", + "@sentry/core": "10.52.0", + "tslib": "^2.4.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@angular/common": ">= 14.x <= 21.x", + "@angular/core": ">= 14.x <= 21.x", + "@angular/router": ">= 14.x <= 21.x", + "rxjs": "^6.5.5 || ^7.x" + } + }, + "node_modules/@sentry/browser": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.52.0.tgz", + "integrity": "sha512-ijL9jN86oXwXQWbwhPlEb70ODJSEmjxQEQdnZkC4gDWbjswcwvRsVJPYk+1xl2ir2iZixRIHipVxDcLwian35g==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "10.52.0", + "@sentry-internal/feedback": "10.52.0", + "@sentry-internal/replay": "10.52.0", + "@sentry-internal/replay-canvas": "10.52.0", + "@sentry/core": "10.52.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/core": { + "version": "10.52.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.52.0.tgz", + "integrity": "sha512-VA/kAqLhkMnRWY2RXdBLyTemR9D4m7MVRy/gyapoq9yvllVPx9WXbvKgnMP2LQp7mFgT/oLFvw58aQKaYTGn3A==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@sigstore/bundle": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", diff --git a/client/web/package.json b/client/web/package.json index 623f1edd..758a2944 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -33,6 +33,7 @@ "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@ngxpert/hot-toast": "^6.1.0", + "@sentry/angular": "^10.52.0", "async-mutex": "^0.5.0", "autolinker": "^4.1.5", "compression": "^1.8.1", diff --git a/client/web/src/app/app.config.ts b/client/web/src/app/app.config.ts index 081131f3..7706f595 100644 --- a/client/web/src/app/app.config.ts +++ b/client/web/src/app/app.config.ts @@ -1,11 +1,13 @@ import { ApplicationConfig, + ErrorHandler, importProvidersFrom, inject, provideAppInitializer, provideZoneChangeDetection, } from '@angular/core'; import { provideRouter, withPreloading } from '@angular/router'; +import * as Sentry from '@sentry/angular'; import { routes } from './app.routes'; import { SelectivePreloadingStrategy } from './core/services/ui/selective-preloading.strategy'; @@ -42,6 +44,10 @@ export function initializeLanguage(languageService: LanguageService): () => Prom export const appConfig: ApplicationConfig = { providers: [ + { + provide: ErrorHandler, + useValue: Sentry.createErrorHandler({ showDialog: false }), + }, provideHttpClient(withFetch()), provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), provideRouter(routes, withPreloading(SelectivePreloadingStrategy)), diff --git a/client/web/src/environments/environment.docker-dev.ts b/client/web/src/environments/environment.docker-dev.ts index 2cfe72d9..ab131c47 100644 --- a/client/web/src/environments/environment.docker-dev.ts +++ b/client/web/src/environments/environment.docker-dev.ts @@ -7,4 +7,9 @@ export const environment = { enableSourceMaps: true, disableFileDetails: false, disableConsoleLogging: false, + sentry: { + enabled: false, + dsn: '', + environment: 'docker-dev', + }, }; diff --git a/client/web/src/environments/environment.prod.ts b/client/web/src/environments/environment.prod.ts index 28bde15a..ff694c99 100644 --- a/client/web/src/environments/environment.prod.ts +++ b/client/web/src/environments/environment.prod.ts @@ -7,4 +7,9 @@ export const environment = { enableSourceMaps: false, disableFileDetails: true, disableConsoleLogging: true, + sentry: { + enabled: true, + dsn: '', + environment: 'production', + }, }; diff --git a/client/web/src/environments/environment.ts b/client/web/src/environments/environment.ts index 11af19ff..e34fe4e8 100644 --- a/client/web/src/environments/environment.ts +++ b/client/web/src/environments/environment.ts @@ -7,4 +7,9 @@ export const environment = { enableSourceMaps: true, disableFileDetails: false, disableConsoleLogging: false, + sentry: { + enabled: false, + dsn: '', + environment: 'development', + }, }; diff --git a/client/web/src/main.ts b/client/web/src/main.ts index 90ebc0ae..ea623c36 100644 --- a/client/web/src/main.ts +++ b/client/web/src/main.ts @@ -1,8 +1,44 @@ import { bootstrapApplication } from '@angular/platform-browser'; +import * as Sentry from '@sentry/angular'; import { appConfig } from './app/app.config'; import { AppComponent } from './app/app.component'; import { TranslateService } from '@ngx-translate/core'; import { LANGUAGE_PREFERENCE_KEY } from './app/utils/constants'; +import { environment } from './environments/environment'; + +// Initialize Sentry before bootstrapping the app so it can capture errors +if (environment.sentry?.enabled && environment.sentry.dsn) { + Sentry.init({ + dsn: environment.sentry.dsn, + environment: environment.sentry.environment, + release: 'web@0.17.0', + sendDefaultPii: false, + maxBreadcrumbs: 50, + tracesSampleRate: 0, + replaysSessionSampleRate: 0, + replaysOnErrorSampleRate: 0, + beforeSend(event) { + // Strip user-identifying data before the event leaves the browser. + event.user = { ip_address: '127.0.0.1' }; + delete event.server_name; + if (event.request) { + delete event.request.cookies; + delete event.request.headers; + delete event.request.data; + delete event.request.query_string; + } + // Strip browser-derived locale signals from the device / culture contexts + if (event.contexts?.['device']) { + delete event.contexts['device']['timezone']; + delete event.contexts['device']['locale']; + } + if (event.contexts?.['culture']) { + delete event.contexts['culture']; + } + return event; + }, + }); +} function getStoredLanguage(): string { if (typeof window !== 'undefined' && window.localStorage) { From 45ea15a622628af856d65039f2e8fc6f6ac126aa Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 03:32:44 +0300 Subject: [PATCH 05/23] Docs: Disclose Sentry error reporting in privacy/terms - README: add Observability feature, Sentry in Tech Stack, env-file workflow in Quick Start, Error Diagnostics under Security - DISCLAIMER.md: amend 5 and add new 11 (Error Diagnostics & Third-Party Processors) covering what is/isn't sent to Sentry, EU storage region, retention, and self-host opt-out - Privacy & Terms page: new "Error Diagnostics" section between Privacy and Terms; updated PRIVACY_THIRD_PARTIES wording - Bump LAST_UPDATED to May 8, 2026 across en, ar, es, fr, ru, zh-CN --- DISCLAIMER.md | 28 ++++++++------ README.md | 38 ++++++++++++++++--- .../src/app/core/i18n/localizations/ar.json | 9 ++++- .../src/app/core/i18n/localizations/en.json | 9 ++++- .../src/app/core/i18n/localizations/es.json | 9 ++++- .../src/app/core/i18n/localizations/fr.json | 9 ++++- .../src/app/core/i18n/localizations/ru.json | 9 ++++- .../app/core/i18n/localizations/zh-CN.json | 9 ++++- .../privacy-and-terms.component.html | 12 ++++++ 9 files changed, 103 insertions(+), 29 deletions(-) diff --git a/DISCLAIMER.md b/DISCLAIMER.md index 5a81a3d9..4033956c 100644 --- a/DISCLAIMER.md +++ b/DISCLAIMER.md @@ -4,14 +4,12 @@ This document outlines the legal disclaimer for the use of **PastePoint**, a peer-to-peer file sharing and messaging platform focused on privacy, speed, and local connectivity. By using PastePoint, you agree to the terms outlined below. If you do not agree with these terms, please discontinue use. - ## 1. No Warranty PastePoint is provided “as is,” without any warranty of any kind, express or implied. This includes, but is not limited to, warranties of merchantability, fitness for a particular purpose, and non-infringement. The developers make no guarantees about the reliability, availability, or security of the service. - ## 2. Limitation of Liability Under no circumstances shall the creators, maintainers, or contributors of PastePoint be held liable for any damages or claims resulting from the use or misuse of this software, including but not limited to: @@ -23,7 +21,6 @@ Under no circumstances shall the creators, maintainers, or contributors of Paste Users accept full responsibility for their actions and content shared via PastePoint. - ## 3. Intended Use PastePoint is designed for: @@ -34,7 +31,6 @@ PastePoint is designed for: It is **not recommended** for use over public or untrusted networks unless you fully understand and accept the associated risks. - ## 4. User Responsibility By using PastePoint, you agree to: @@ -45,7 +41,6 @@ By using PastePoint, you agree to: The developers are not responsible for monitoring or controlling user activity. - ## 5. No Data Retention PastePoint does **not** store: @@ -55,8 +50,7 @@ PastePoint does **not** store: - Metadata - IP logs or session history -All file transfers occur directly and are ephemeral. However, your device, browser, or network may log information independently. - +All file transfers occur directly and are ephemeral. However, your device, browser, or network may log information independently. The application may send anonymized error reports to a third-party error-tracking service to help maintainers diagnose crashes; see §11 below. ## 6. Encryption & Security @@ -68,14 +62,12 @@ PastePoint uses: Security is a shared responsibility between the app and the user. For sensitive usage, use trusted certificates and ensure your host system is secure. - ## 7. Open Source Licensing PastePoint uses and integrates third-party open-source software. Each component is governed by its own license (e.g., MIT, Apache, GPL). The main project is released under the **GPL-3.0** license. Refer to the [LICENSE](LICENSE) file for more. - ## 8. Contributions By contributing, you agree to: @@ -84,7 +76,6 @@ By contributing, you agree to: - Avoid submitting malicious or unauthorized content - Follow the project’s code quality and community standards - ## 9. Production Use Notice PastePoint is under active development and is primarily intended for local or experimental use. Production deployments should: @@ -94,7 +85,6 @@ PastePoint is under active development and is primarily intended for local or ex - Regularly audit code and dependencies - Use isolated network setups if handling sensitive files - ## 10. Contact For security concerns, legal questions, or bug reports: @@ -102,5 +92,21 @@ For security concerns, legal questions, or bug reports: - GitHub Issues: [https://github.com/SloMR/pastepoint/issues](https://github.com/SloMR/pastepoint/issues) - Email: [sulaimanromaih@gmail.com](mailto:sulaimanromaih@gmail.com) +## 11. Error Diagnostics & Third-Party Processors + +PastePoint may send technical error reports to **Sentry** (operated by Functional Software, Inc. d/b/a Sentry). These reports are stored in Sentry's **European Union data region** and contain: + +- Crash and exception details (error type, message, stack trace) +- Application version, environment (development / production), and runtime info (OS, browser, language) + +The reports do **not** contain: + +- File contents or filenames +- Chat messages +- Room or session identifiers +- User accounts, names, or email addresses +- IP addresses or geolocation (the SDK and server-side scrubbing both strip these) + +Error reports help us identify and fix bugs. They are retained for a limited time and then deleted automatically. Operators of self-hosted PastePoint instances may disable error reporting entirely by setting `SENTRY_ENABLED=false` in their environment configuration. PastePoint is a tool. Please use it wisely, lawfully, and responsibly. diff --git a/README.md b/README.md index 135906a3..5575a8d6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,6 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net ### Core Features: - **Local Network Communication**: - - Establish WebSocket-based local chat between computers on the same network - List available sessions, create new sessions, or join existing ones - Multiple rooms within a session — create, list, and switch between rooms @@ -30,19 +29,22 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net - Resilient WebSocket signaling with automatic reconnect, heartbeat, and bfcache support - **File Sharing**: - - Peer-to-peer WebRTC connections for secure file transfers - Drag & drop file upload with real-time progress tracking - File offer system with accept/decline options - Chunk-based file transfer with progress tracking and cancellation support - **Security**: - - End-to-end encryption for all file transfers via WebRTC - SSL/TLS encryption for WebSocket signaling - Self-signed certificate generation included - Input validation and rate limiting +- **Observability**: + - Optional Sentry-based error tracking (EU-hosted, off by default in dev) + - Privacy-tight defaults: no IPs, no geo, no request bodies, no user identifiers + - Toggle per-environment via `SENTRY_ENABLED` / `SENTRY_DSN` + - **Cross-Platform Compatibility**: - Runs seamlessly on Linux, macOS, and Windows with Dockerized support - Responsive design for mobile and desktop @@ -72,6 +74,7 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net - **Security**: OpenSSL for TLS termination - **Utilities**: UUID generation, Serde serialization - **Rate Limiting**: Actix-governor for request throttling +- **Error Tracking**: `sentry` + `sentry-actix` with privacy-tight redaction ### Clients @@ -89,6 +92,7 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net - **QR Sharing**: `qrcode` for generation, `jsqr` for camera-based scanning - **Integrity**: `hash-wasm` for fast file hashing - **Notifications**: Hot-toast for real-time feedback +- **Error Tracking**: `@sentry/angular` with privacy-tight redaction ### Infrastructure @@ -197,14 +201,33 @@ pastepoint/ ./scripts/configure-network.sh ``` - This will prompt you to enter your local IP address and update all necessary configuration files. + The script will create `.env.development` from the committed template if it + doesn't already exist, then prompt for your local IP and update all + necessary configuration files. To bootstrap the environment manually: + + ```bash + cp .env.development.example .env.development + ``` 4. Build and Start Services: ```bash - make dev # or make prod + make dev # uses .env.development (gitignored, machine-local) + make prod # uses .env.production (gitignored, host-local — see below) ``` + For production deploys, one-time setup: + + ```bash + cp .env.production.example .env.production + chmod 600 .env.production + # edit .env.production: SERVER_NAME, SENTRY_DSN, etc. + ``` + + Real DSNs and host-specific values live only in the gitignored + `.env.development` / `.env.production` files. The committed `.example` + templates document which variables exist. + 5. Access PastePoint: - Frontend: - Localhost: [https://localhost](https://localhost) @@ -227,7 +250,6 @@ pastepoint/ ## Security Considerations - **Certificate Management**: - - Replace self-signed certificates with proper SSL certificates in production - Keep private keys secure and never commit them to version control @@ -236,6 +258,10 @@ pastepoint/ - No data is stored permanently on servers - Session data is cleared on server restart or leaving the session +- **Error Diagnostics (Sentry)**: + - Server and web SDKs scrub: user identifiers, IP addresses, geo, + request bodies, headers, cookies, query strings, locale, timezone + ## License This project is licensed under the GPL-3.0 License. See the [LICENSE](LICENSE) file for details. diff --git a/client/web/src/app/core/i18n/localizations/ar.json b/client/web/src/app/core/i18n/localizations/ar.json index f433881a..05a8d0c1 100644 --- a/client/web/src/app/core/i18n/localizations/ar.json +++ b/client/web/src/app/core/i18n/localizations/ar.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "الخصوصية والشروط", - "LAST_UPDATED": "آخر تحديث: 26 يوليو 2025", + "LAST_UPDATED": "آخر تحديث: 8 مايو 2026", "DISCLAIMER_TITLE": "إخلاء المسؤولية", "DISCLAIMER_INTRO": "يُقدَّم برنامج PastePoint «كما هو» و«كما هو متاح» دون أي ضمانات صريحة أو ضمنية.", "DISCLAIMER_NO_GUARANTEES": "لا نضمن أن تكون الخدمة متاحة دون انقطاع أو خالية من الأخطاء أو آمنة بالكامل.", @@ -101,7 +101,12 @@ "PRIVACY_NO_RETENTION": "لا يتم تخزين أي محتوى (ملفات أو رسائل أو رموز جلسات) تُرسَل عبر الخدمة على خوادمنا. وبمجرد اكتمال النقل بين الأقران، لا يمكننا الوصول إلى المحتوى.", "PRIVACY_ENCRYPTION": "تتم جميع عمليات النقل عبر قنوات WebRTC محمية ببروتوكولي DTLS وSRTP، مما يوفّر تشفيراً من طرف إلى طرف بين الأجهزة المشاركة.", "PRIVACY_COLLECTION": "قد تمر بيانات اتّصال مثل عناوين IP عبر خادم الإشارة الخاص بنا لغرض وحيد هو إتمام مصافحة WebRTC. ويتم الاحتفاظ بهذه البيانات بشكل مؤقت ولا تُستخدم للتعرّف عليك.", - "PRIVACY_THIRD_PARTIES": "لا نقوم ببيع أو تأجير أو مشاركة معلوماتك مع أي جهات خارجية. قد نعتمد على تحليلات ذات استضافة ذاتية (مثل Umami, GAnalytics, etc.) لإحصاءات استخدام مجهولة تساعدنا على تحسين الخدمة.", + "PRIVACY_THIRD_PARTIES": "لا نقوم ببيع أو تأجير معلوماتك الشخصية. يستخدم PastePoint خدمتين خارجيتين مستضافتين في الاتحاد الأوروبي لتشغيل الخدمة وتحسينها: Sentry لتقارير الأخطاء التقنية مجهولة الهوية (انظر قسم تشخيص الأخطاء أدناه)، وUmami Cloud لإحصاءات الاستخدام التجميعية الصديقة للخصوصية وبدون ملفات تعريف الارتباط. لا تتلقى أيٌّ من هاتين الخدمتين محتوى ملفاتك أو رسائلك أو معرّفاتك، ولا تستخدمان ملفات تعريف الارتباط.", + "DIAGNOSTICS_TITLE": "تشخيص الأخطاء", + "DIAGNOSTICS_INTRO": "عند تمكين الإبلاغ عن الأخطاء، يرسل PastePoint تفاصيل الأعطال والاستثناءات إلى Sentry (تُخزَّن البيانات في الاتحاد الأوروبي). يساعدنا ذلك على إصلاح الأخطاء بسرعة.", + "DIAGNOSTICS_INCLUDED": "تتضمن التقارير: نوع الخطأ ورسالته، وتتبع المكدس (stack trace)، وإصدار التطبيق، والبيئة (تطوير أو إنتاج)، ومعلومات تشغيل أساسية (نظام التشغيل، المتصفح، اللغة).", + "DIAGNOSTICS_EXCLUDED": "لا تتضمن التقارير أبدًا: محتوى الملفات، أو أسماء الملفات، أو رسائل المحادثة، أو معرّفات الغرف أو الجلسات، أو معلومات الحساب، أو عناوين IP، أو الموقع الجغرافي.", + "DIAGNOSTICS_RETENTION": "يتم الاحتفاظ بتقارير الأخطاء لفترة محدودة ثم تُحذف تلقائيًا. يمكن لمشغّلي النسخ المستضافة ذاتيًا تعطيل الإبلاغ عن الأخطاء بالكامل عبر الإعدادات.", "PRIVACY_CHANGES": "قد نقوم بتحديث سياسة الخصوصية من وقت لآخر. في حال أجرينا تغييرات جوهرية، سننشر السياسة الجديدة داخل التطبيق. استمرارك في الاستخدام بعد تاريخ السريان يعني قبولك للسياسة المعدَّلة.", "TERMS_TITLE": "شروط الخدمة", "TERMS_AGREEMENT": "بدخولك إلى PastePoint أو استخدامك له فإنك توافق على الالتزام بهذه الشروط. إذا كنت لا توافق، فلا تستخدم الخدمة.", diff --git a/client/web/src/app/core/i18n/localizations/en.json b/client/web/src/app/core/i18n/localizations/en.json index fbacd92f..c4f69c35 100644 --- a/client/web/src/app/core/i18n/localizations/en.json +++ b/client/web/src/app/core/i18n/localizations/en.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "Privacy & Terms", - "LAST_UPDATED": "Last updated: July 26, 2025", + "LAST_UPDATED": "Last updated: May 8, 2026", "DISCLAIMER_TITLE": "Disclaimer", "DISCLAIMER_INTRO": "The PastePoint software (\"Service\") is provided on an \"AS IS\" and \"AS AVAILABLE\" basis without warranties of any kind, express or implied.", "DISCLAIMER_NO_GUARANTEES": "We do not guarantee that the Service will be uninterrupted, secure, or free of errors.", @@ -101,8 +101,13 @@ "PRIVACY_NO_RETENTION": "No content sent through the Service (files, messages, or session codes) is stored on our servers. Once the peer‑to‑peer transfer is complete, the content is no longer accessible to us.", "PRIVACY_ENCRYPTION": "All transfers occur over WebRTC data channels secured by DTLS and SRTP, providing end‑to‑end encryption between the participating devices.", "PRIVACY_COLLECTION": "Connection metadata such as IP addresses may transit our signaling server solely to coordinate the WebRTC handshake. This metadata is transient, retained only as long as necessary to establish the connection, and is never used to identify you.", - "PRIVACY_THIRD_PARTIES": "We do not sell, rent, or share your personal information with third parties. Open‑source, self‑hosted analytics (e.g., Umami, GAnalytics, etc.) may aggregate anonymized usage statistics to help us improve the Service.", + "PRIVACY_THIRD_PARTIES": "We do not sell or rent your personal information. PastePoint uses two external services hosted in the European Union to operate and improve the Service: Sentry for anonymized technical error reports (see the Error Diagnostics section below) and Umami Cloud for cookieless, privacy‑friendly aggregate usage analytics. Neither service receives your file contents, messages, identifiers, nor sets cookies.", "PRIVACY_CHANGES": "We may update this Privacy Policy from time to time. If we make material changes, we will post the new policy in the application. Continued use after the effective date constitutes acceptance of the revised policy.", + "DIAGNOSTICS_TITLE": "Error Diagnostics", + "DIAGNOSTICS_INTRO": "When error reporting is enabled, PastePoint sends crash and exception details to Sentry (data stored in the European Union). This helps us fix bugs quickly.", + "DIAGNOSTICS_INCLUDED": "Reports include: error type and message, stack trace, app version, environment (development or production), and basic runtime info (OS, browser, language).", + "DIAGNOSTICS_EXCLUDED": "Reports never include: file contents, file names, chat messages, room or session identifiers, account information, IP addresses, or geolocation.", + "DIAGNOSTICS_RETENTION": "Error reports are retained for a limited time and then automatically deleted. Self‑hosted operators may disable error reporting entirely via configuration.", "TERMS_TITLE": "Terms of Service", "TERMS_AGREEMENT": "By accessing or using PastePoint you agree to be bound by these Terms of Service (\"Terms\"). If you do not agree, do not use the Service.", "TERMS_INTENDED_USE": "The Service is intended for lawful, personal or organizational use to share files and messages directly between devices.", diff --git a/client/web/src/app/core/i18n/localizations/es.json b/client/web/src/app/core/i18n/localizations/es.json index 7792fd97..1abbedab 100644 --- a/client/web/src/app/core/i18n/localizations/es.json +++ b/client/web/src/app/core/i18n/localizations/es.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "Privacidad y términos", - "LAST_UPDATED": "Última actualización: 26 de julio de 2025", + "LAST_UPDATED": "Última actualización: 8 de mayo de 2026", "DISCLAIMER_TITLE": "Aviso legal", "DISCLAIMER_INTRO": "El software PastePoint (\"el Servicio\") se ofrece \"TAL CUAL\" y \"SEGÚN DISPONIBILIDAD\", sin garantías de ningún tipo, ya sean expresas o implícitas.", "DISCLAIMER_NO_GUARANTEES": "No garantizamos que el Servicio sea ininterrumpido, seguro o esté libre de errores.", @@ -101,8 +101,13 @@ "PRIVACY_NO_RETENTION": "No almacenamos en nuestros servidores ningún contenido enviado a través del Servicio (archivos, mensajes ni códigos de sesión). Una vez completada la transferencia entre pares, el contenido deja de ser accesible para nosotros.", "PRIVACY_ENCRYPTION": "Todas las transferencias se realizan a través de canales de datos WebRTC protegidos por DTLS y SRTP, lo que proporciona cifrado de extremo a extremo entre los dispositivos participantes.", "PRIVACY_COLLECTION": "Los metadatos de conexión, como las direcciones IP, pueden pasar por nuestro servidor de señalización únicamente para coordinar el intercambio WebRTC. Estos metadatos son transitorios, se conservan solo el tiempo necesario para establecer la conexión y nunca se utilizan para identificarte.", - "PRIVACY_THIRD_PARTIES": "No vendemos, alquilamos ni compartimos tu información personal con terceros. Las analíticas autoalojadas y de código abierto (por ejemplo, Umami, GAnalytics, etc.) pueden agregar estadísticas anónimas de uso para ayudarnos a mejorar el Servicio.", + "PRIVACY_THIRD_PARTIES": "No vendemos ni alquilamos tu información personal. PastePoint utiliza dos servicios externos alojados en la Unión Europea para operar y mejorar el Servicio: Sentry para informes técnicos de errores anonimizados (consulta la sección Diagnóstico de errores más abajo) y Umami Cloud para analíticas de uso agregadas, sin cookies y respetuosas con la privacidad. Ninguno de los dos servicios recibe el contenido de tus archivos, mensajes, identificadores ni utiliza cookies.", "PRIVACY_CHANGES": "Podemos actualizar esta Política de privacidad de vez en cuando. Si realizamos cambios sustanciales, publicaremos la nueva política en la aplicación. El uso continuado tras la fecha de entrada en vigor constituye la aceptación de la política revisada.", + "DIAGNOSTICS_TITLE": "Diagnóstico de errores", + "DIAGNOSTICS_INTRO": "Cuando el reporte de errores está habilitado, PastePoint envía detalles de fallos y excepciones a Sentry (datos almacenados en la Unión Europea). Esto nos ayuda a corregir errores rápidamente.", + "DIAGNOSTICS_INCLUDED": "Los informes incluyen: tipo y mensaje del error, traza de pila, versión de la aplicación, entorno (desarrollo o producción) e información básica de ejecución (sistema operativo, navegador, idioma).", + "DIAGNOSTICS_EXCLUDED": "Los informes nunca incluyen: contenido de archivos, nombres de archivos, mensajes de chat, identificadores de sala o sesión, información de cuenta, direcciones IP ni geolocalización.", + "DIAGNOSTICS_RETENTION": "Los informes de errores se conservan durante un tiempo limitado y luego se eliminan automáticamente. Los operadores autoalojados pueden desactivar el reporte de errores por completo mediante la configuración.", "TERMS_TITLE": "Términos del servicio", "TERMS_AGREEMENT": "Al acceder o utilizar PastePoint aceptas cumplir estos Términos del servicio (\"Términos\"). Si no estás de acuerdo, no utilices el Servicio.", "TERMS_INTENDED_USE": "El Servicio está pensado para un uso lícito, personal o de organización, para compartir archivos y mensajes directamente entre dispositivos.", diff --git a/client/web/src/app/core/i18n/localizations/fr.json b/client/web/src/app/core/i18n/localizations/fr.json index 445402a4..18fa1895 100644 --- a/client/web/src/app/core/i18n/localizations/fr.json +++ b/client/web/src/app/core/i18n/localizations/fr.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "Confidentialité et conditions", - "LAST_UPDATED": "Dernière mise à jour : 26 juillet 2025", + "LAST_UPDATED": "Dernière mise à jour : 8 mai 2026", "DISCLAIMER_TITLE": "Avertissement", "DISCLAIMER_INTRO": "Le logiciel PastePoint (« le Service ») est fourni « TEL QUEL » et « SELON DISPONIBILITÉ », sans aucune garantie, expresse ou implicite.", "DISCLAIMER_NO_GUARANTEES": "Nous ne garantissons pas que le Service sera ininterrompu, sécurisé ou exempt d'erreurs.", @@ -101,8 +101,13 @@ "PRIVACY_NO_RETENTION": "Aucun contenu transmis via le Service (fichiers, messages ou codes de session) n'est stocké sur nos serveurs. Une fois le transfert pair-à-pair terminé, le contenu nous devient inaccessible.", "PRIVACY_ENCRYPTION": "Tous les transferts ont lieu sur des canaux de données WebRTC sécurisés par DTLS et SRTP, assurant un chiffrement de bout en bout entre les appareils participants.", "PRIVACY_COLLECTION": "Les métadonnées de connexion telles que les adresses IP peuvent transiter par notre serveur de signalisation uniquement pour coordonner la négociation WebRTC. Ces métadonnées sont transitoires, conservées le temps strictement nécessaire à l'établissement de la connexion, et ne sont jamais utilisées pour vous identifier.", - "PRIVACY_THIRD_PARTIES": "Nous ne vendons, ne louons ni ne partageons vos informations personnelles avec des tiers. Des outils d'analyse open source auto-hébergés (par exemple Umami, GAnalytics, etc.) peuvent agréger des statistiques d'utilisation anonymisées pour nous aider à améliorer le Service.", + "PRIVACY_THIRD_PARTIES": "Nous ne vendons ni ne louons vos informations personnelles. PastePoint utilise deux services externes hébergés dans l'Union européenne pour exploiter et améliorer le Service : Sentry pour les rapports d'erreurs techniques anonymisés (voir la section Diagnostic des erreurs ci-dessous) et Umami Cloud pour des statistiques d'utilisation agrégées, sans cookies et respectueuses de la vie privée. Aucun de ces services ne reçoit le contenu de vos fichiers, vos messages, vos identifiants, ni n'utilise de cookies.", "PRIVACY_CHANGES": "Nous pouvons mettre à jour cette politique de temps à autre. En cas de modifications importantes, nous publierons la nouvelle politique dans l'application. Toute utilisation continue après la date d'entrée en vigueur vaut acceptation.", + "DIAGNOSTICS_TITLE": "Diagnostic des erreurs", + "DIAGNOSTICS_INTRO": "Lorsque le rapport d'erreurs est activé, PastePoint envoie les détails des plantages et exceptions à Sentry (données stockées dans l'Union européenne). Cela nous aide à corriger les bugs rapidement.", + "DIAGNOSTICS_INCLUDED": "Les rapports incluent : le type et le message de l'erreur, la trace d'appels, la version de l'application, l'environnement (développement ou production) et des informations d'exécution de base (système d'exploitation, navigateur, langue).", + "DIAGNOSTICS_EXCLUDED": "Les rapports n'incluent jamais : le contenu des fichiers, les noms de fichiers, les messages de chat, les identifiants de salon ou de session, les informations de compte, les adresses IP ni la géolocalisation.", + "DIAGNOSTICS_RETENTION": "Les rapports d'erreurs sont conservés pendant une durée limitée puis automatiquement supprimés. Les opérateurs auto-hébergés peuvent désactiver entièrement le rapport d'erreurs via la configuration.", "TERMS_TITLE": "Conditions d'utilisation", "TERMS_AGREEMENT": "En accédant à PastePoint ou en l'utilisant, vous acceptez d'être lié par ces Conditions d'utilisation. Si vous n'êtes pas d'accord, n'utilisez pas le Service.", "TERMS_INTENDED_USE": "Le Service est destiné à un usage légal, personnel ou organisationnel, pour partager des fichiers et des messages directement entre appareils.", diff --git a/client/web/src/app/core/i18n/localizations/ru.json b/client/web/src/app/core/i18n/localizations/ru.json index edba6125..f06037c6 100644 --- a/client/web/src/app/core/i18n/localizations/ru.json +++ b/client/web/src/app/core/i18n/localizations/ru.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "Конфиденциальность и условия", - "LAST_UPDATED": "Последнее обновление: 26 июля 2025 г.", + "LAST_UPDATED": "Последнее обновление: 8 мая 2026 г.", "DISCLAIMER_TITLE": "Отказ от ответственности", "DISCLAIMER_INTRO": "Программное обеспечение PastePoint («Сервис») предоставляется «КАК ЕСТЬ» и «ПО ДОСТУПНОСТИ» без каких-либо явных или подразумеваемых гарантий.", "DISCLAIMER_NO_GUARANTEES": "Мы не гарантируем, что Сервис будет работать без перебоев, безопасно или без ошибок.", @@ -101,8 +101,13 @@ "PRIVACY_NO_RETENTION": "Никакой контент, отправленный через Сервис (файлы, сообщения, коды сессий), не хранится на наших серверах. После завершения пиринговой передачи контент становится недоступен для нас.", "PRIVACY_ENCRYPTION": "Все передачи происходят через каналы данных WebRTC, защищённые DTLS и SRTP, что обеспечивает сквозное шифрование между участвующими устройствами.", "PRIVACY_COLLECTION": "Метаданные подключения, такие как IP-адреса, могут проходить через наш сигнальный сервер исключительно для координации установления WebRTC-соединения. Эти метаданные временные, хранятся только столько, сколько необходимо для установления соединения, и не используются для вашей идентификации.", - "PRIVACY_THIRD_PARTIES": "Мы не продаём, не сдаём в аренду и не передаём вашу личную информацию третьим лицам. Самостоятельно размещаемая аналитика с открытым исходным кодом (например, Umami, GAnalytics и т. п.) может агрегировать обезличенную статистику использования для улучшения Сервиса.", + "PRIVACY_THIRD_PARTIES": "Мы не продаём и не сдаём в аренду вашу личную информацию. PastePoint использует два внешних сервиса, размещённых в Европейском союзе, для работы и улучшения Сервиса: Sentry — для обезличенных технических отчётов об ошибках (см. раздел «Диагностика ошибок» ниже), и Umami Cloud — для агрегированной, дружественной к конфиденциальности аналитики использования без файлов cookie. Ни один из этих сервисов не получает содержимое ваших файлов, сообщений, идентификаторов и не устанавливает cookie.", "PRIVACY_CHANGES": "Мы можем периодически обновлять эту политику. При существенных изменениях мы опубликуем новую политику в приложении. Продолжение использования после даты вступления в силу означает согласие с изменённой политикой.", + "DIAGNOSTICS_TITLE": "Диагностика ошибок", + "DIAGNOSTICS_INTRO": "Когда отправка отчётов об ошибках включена, PastePoint отправляет сведения о сбоях и исключениях в Sentry (данные хранятся в Европейском союзе). Это помогает нам быстро исправлять ошибки.", + "DIAGNOSTICS_INCLUDED": "Отчёты содержат: тип и сообщение ошибки, стек вызовов, версию приложения, среду (разработка или продакшн) и базовые сведения о среде выполнения (ОС, браузер, язык).", + "DIAGNOSTICS_EXCLUDED": "Отчёты никогда не содержат: содержимое файлов, имена файлов, сообщения чата, идентификаторы комнат или сессий, информацию об учётной записи, IP-адреса или геолокацию.", + "DIAGNOSTICS_RETENTION": "Отчёты об ошибках хранятся ограниченное время, а затем автоматически удаляются. Операторы самостоятельных установок могут полностью отключить отправку отчётов через конфигурацию.", "TERMS_TITLE": "Условия использования", "TERMS_AGREEMENT": "Используя PastePoint, вы соглашаетесь с настоящими Условиями. Если вы не согласны, не используйте Сервис.", "TERMS_INTENDED_USE": "Сервис предназначен для законного личного или организационного использования с целью прямого обмена файлами и сообщениями между устройствами.", diff --git a/client/web/src/app/core/i18n/localizations/zh-CN.json b/client/web/src/app/core/i18n/localizations/zh-CN.json index a7a6e357..54ec12f9 100644 --- a/client/web/src/app/core/i18n/localizations/zh-CN.json +++ b/client/web/src/app/core/i18n/localizations/zh-CN.json @@ -86,7 +86,7 @@ "_PRIVACY_AND_TERMS_SECTION": "==== PRIVACY AND TERMS ====", "PRIVACY_AND_TERMS": "隐私与条款", - "LAST_UPDATED": "最后更新:2025 年 7 月 26 日", + "LAST_UPDATED": "最后更新:2026 年 5 月 8 日", "DISCLAIMER_TITLE": "免责声明", "DISCLAIMER_INTRO": "PastePoint 软件(下称「服务」)按「现状」和「可用」基础提供,不附带任何明示或暗示的保证。", "DISCLAIMER_NO_GUARANTEES": "我们不保证服务不会中断,也不保证其安全或不会出错。", @@ -101,8 +101,13 @@ "PRIVACY_NO_RETENTION": "通过本服务发送的任何内容(文件、消息或会话代码)均不会存储在我们的服务器上。点对点传输完成后,这些内容对我们不再可访问。", "PRIVACY_ENCRYPTION": "所有传输都通过受 DTLS 与 SRTP 保护的 WebRTC 数据通道进行,在参与设备之间提供端到端加密。", "PRIVACY_COLLECTION": "诸如 IP 地址等连接元数据可能会经过我们的信令服务器,仅用于协调 WebRTC 握手。这些元数据是临时性的,仅在建立连接所需的时间内保留,绝不会用于识别您的身份。", - "PRIVACY_THIRD_PARTIES": "我们不会出售、出租或与第三方共享您的个人信息。开源的自托管分析工具(例如 Umami、GAnalytics 等)可能会汇总匿名化的使用统计数据,以帮助我们改进服务。", + "PRIVACY_THIRD_PARTIES": "我们不会出售或出租您的个人信息。PastePoint 使用两项托管在欧盟的外部服务来运行和改进本服务:Sentry 用于匿名化的技术错误报告(请参阅下方的「错误诊断」部分),以及 Umami Cloud 用于无 Cookie、注重隐私的汇总使用分析。这两项服务都不会接收您的文件内容、消息或标识符,也不会设置 Cookie。", "PRIVACY_CHANGES": "我们可能会不时更新本隐私政策。如果有重大变更,我们将在应用程序中发布新政策。在生效日期之后继续使用即视为接受修订后的政策。", + "DIAGNOSTICS_TITLE": "错误诊断", + "DIAGNOSTICS_INTRO": "启用错误报告后,PastePoint 会将崩溃和异常详情发送至 Sentry(数据存储在欧盟)。这有助于我们快速修复错误。", + "DIAGNOSTICS_INCLUDED": "报告包含:错误类型和消息、堆栈跟踪、应用版本、环境(开发或生产)以及基本运行时信息(操作系统、浏览器、语言)。", + "DIAGNOSTICS_EXCLUDED": "报告绝不包含:文件内容、文件名、聊天消息、房间或会话标识符、账户信息、IP 地址或地理位置。", + "DIAGNOSTICS_RETENTION": "错误报告会保留有限时间,然后自动删除。自托管的运营者可通过配置完全禁用错误报告。", "TERMS_TITLE": "服务条款", "TERMS_AGREEMENT": "通过访问或使用 PastePoint,您同意受本服务条款(「条款」)约束。如不同意,请勿使用本服务。", "TERMS_INTENDED_USE": "本服务旨在用于合法的个人或组织用途,以便在设备之间直接共享文件和消息。", diff --git a/client/web/src/app/features/privacy-and-terms/privacy-and-terms.component.html b/client/web/src/app/features/privacy-and-terms/privacy-and-terms.component.html index 517e1ac6..bff4b130 100644 --- a/client/web/src/app/features/privacy-and-terms/privacy-and-terms.component.html +++ b/client/web/src/app/features/privacy-and-terms/privacy-and-terms.component.html @@ -74,6 +74,18 @@

+ {{ 'DIAGNOSTICS_TITLE' | translate }} +

+
+

{{ 'DIAGNOSTICS_INTRO' | translate }}

+

{{ 'DIAGNOSTICS_INCLUDED' | translate }}

+

{{ 'DIAGNOSTICS_EXCLUDED' | translate }}

+

{{ 'DIAGNOSTICS_RETENTION' | translate }}

+
+ +

{{ 'TERMS_TITLE' | translate }} From 877d97f59dca9112f624ae9f05a01727d90571d3 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 04:17:40 +0300 Subject: [PATCH 06/23] Server: Forward logs and traces to Sentry with privacy guarantees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable sentry "log" + "logs" features for structured Logs ingestion - SentryLogger bridge: error → Exception (Issues), warn/info → Log + Breadcrumb (Logs page), debug/trace dropped - enable_logs: true on ClientOptions; sentry-actix middleware switched to with_transaction() so HTTP requests auto-create transactions - traces_sample_rate config (default 0.1); per-op sampler reduces signaling.relay to 1% to stay under free-tier quota - RelaySignalMessage / ValidateAndRelaySignal handlers wrapped in named "signaling.relay" transactions - Privacy hardening for non-Event surfaces (logs/transactions skip before_send): server_name overridden globally, default scope user set with 127.0.0.1 to suppress geo enrichment - Quiet noisy transport crates (reqwest/hyper/h2/rustls) in env_logger --- server/Cargo.lock | 12 +++++++++ server/Cargo.toml | 2 +- server/config/development.toml | 1 + server/config/docker-dev.toml | 1 + server/config/production.toml | 1 + server/src/config.rs | 7 +++++ server/src/handler.rs | 18 +++++++++++++ server/src/main.rs | 49 +++++++++++++++++++++++++++++++--- 8 files changed, 86 insertions(+), 5 deletions(-) diff --git a/server/Cargo.lock b/server/Cargo.lock index c3a45533..c4620b30 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -2615,6 +2615,7 @@ dependencies = [ "sentry-contexts", "sentry-core", "sentry-debug-images", + "sentry-log", "sentry-panic", "sentry-tracing", "tokio", @@ -2682,6 +2683,17 @@ dependencies = [ "sentry-core", ] +[[package]] +name = "sentry-log" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8d7875e460bffe85150b5a452834cc3c9a7696df7693f6024320097bdd2a2d" +dependencies = [ + "bitflags", + "log", + "sentry-core", +] + [[package]] name = "sentry-panic" version = "0.48.1" diff --git a/server/Cargo.toml b/server/Cargo.toml index e4ecae79..f8b7bf10 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -31,7 +31,7 @@ rand = "0.10.1" bytes = "1.11.1" config = "0.15.22" url = "2.5.8" -sentry = "0.48" +sentry = { version = "0.48", features = ["log", "logs"] } sentry-actix = "0.48" [dev-dependencies] diff --git a/server/config/development.toml b/server/config/development.toml index 0b5f9c7f..6770a9d3 100644 --- a/server/config/development.toml +++ b/server/config/development.toml @@ -14,3 +14,4 @@ cors_allowed_origins = "https://127.0.0.1" enabled = false environment = "development" sample_rate = 1.0 +traces_sample_rate = 0.1 diff --git a/server/config/docker-dev.toml b/server/config/docker-dev.toml index 86858209..78f346fb 100644 --- a/server/config/docker-dev.toml +++ b/server/config/docker-dev.toml @@ -14,3 +14,4 @@ cors_allowed_origins = "https://127.0.0.1" enabled = false environment = "docker-dev" sample_rate = 1.0 +traces_sample_rate = 0.1 diff --git a/server/config/production.toml b/server/config/production.toml index c860aae2..27ef289a 100644 --- a/server/config/production.toml +++ b/server/config/production.toml @@ -14,3 +14,4 @@ cors_allowed_origins = "https://pastepoint.com" enabled = true environment = "production" sample_rate = 1.0 +traces_sample_rate = 0.1 diff --git a/server/src/config.rs b/server/src/config.rs index c77ff9e9..872d235d 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -13,6 +13,10 @@ fn default_sentry_sample_rate() -> f32 { 1.0 } +fn default_sentry_traces_sample_rate() -> f32 { + 0.1 +} + #[derive(Clone, Debug, Deserialize)] pub struct SentryConfig { #[serde(default)] @@ -21,6 +25,8 @@ pub struct SentryConfig { pub environment: Option, #[serde(default = "default_sentry_sample_rate")] pub sample_rate: f32, + #[serde(default = "default_sentry_traces_sample_rate")] + pub traces_sample_rate: f32, } impl Default for SentryConfig { @@ -29,6 +35,7 @@ impl Default for SentryConfig { enabled: false, environment: None, sample_rate: default_sentry_sample_rate(), + traces_sample_rate: default_sentry_traces_sample_rate(), } } } diff --git a/server/src/handler.rs b/server/src/handler.rs index 11408c74..d386df16 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -98,6 +98,11 @@ impl Handler for WsChatServer { type Result = (); fn handle(&mut self, msg: RelaySignalMessage, _ctx: &mut Self::Context) { + let tx = sentry::start_transaction(sentry::TransactionContext::new( + "signaling.relay", + "websocket.signal", + )); + let RelaySignalMessage { session_id, from, @@ -110,10 +115,14 @@ impl Handler for WsChatServer { target: "Websocket", "Skipping self-to-self signal from '{from}' to '{to}'" ); + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); return; } self.relay_message_to_user(&session_id, &to, message, &from); + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); } } @@ -132,6 +141,11 @@ impl Handler for WsChatServer { type Result = (); fn handle(&mut self, msg: ValidateAndRelaySignal, _ctx: &mut Self::Context) -> Self::Result { + let tx = sentry::start_transaction(sentry::TransactionContext::new( + "signaling.relay", + "websocket.signal", + )); + let shared_room = self.users_share_room(&msg.session_id, &msg.from_user, &msg.to_user); if !shared_room { @@ -141,10 +155,14 @@ impl Handler for WsChatServer { msg.from_user, msg.to_user ); + tx.set_status(sentry::protocol::SpanStatus::PermissionDenied); + tx.finish(); return; } let relay_msg = ChatMessage(format!("{} {}", WS_PREFIX_SIGNAL_MESSAGE, msg.payload)); self.relay_message_to_user(&msg.session_id, &msg.to_user, relay_msg, &msg.from_user); + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); } } diff --git a/server/src/main.rs b/server/src/main.rs index de65f63d..6bf7d275 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -22,10 +22,18 @@ fn init_sentry(cfg: &SentryConfig) -> Option { } let dsn = std::env::var("SENTRY_DSN").ok().filter(|s| !s.is_empty())?; + let global_traces_rate = cfg.traces_sample_rate; let options = sentry::ClientOptions { release: sentry::release_name!(), environment: cfg.environment.clone().map(Cow::Owned), sample_rate: cfg.sample_rate, + traces_sample_rate: cfg.traces_sample_rate, + enable_logs: true, + server_name: Some(Cow::Borrowed("pastepoint-server")), + traces_sampler: Some(Arc::new(move |ctx| match ctx.name() { + "signaling.relay" => 0.01, + _ => global_traces_rate, + })), send_default_pii: false, attach_stacktrace: true, max_breadcrumbs: 50, @@ -47,9 +55,15 @@ fn init_sentry(cfg: &SentryConfig) -> Option { } log::info!( target: "Sentry", - "Captured event {} level={:?} message={:?}", + "Captured {} id={} level={:?} tx={:?} msg={:?}", + if event.transaction.is_some() && event.exception.values.is_empty() { + "transaction" + } else { + "event" + }, event.event_id, event.level, + event.transaction.as_deref().unwrap_or(""), event.message.as_deref().unwrap_or("") ); Some(event) @@ -57,7 +71,17 @@ fn init_sentry(cfg: &SentryConfig) -> Option { ..Default::default() }; - Some(sentry::init((dsn, options))) + let guard = sentry::init((dsn, options)); + sentry::configure_scope(|scope| { + scope.set_user(Some(sentry::protocol::User { + ip_address: Some(sentry::protocol::IpAddress::Exact(IpAddr::V4( + Ipv4Addr::LOCALHOST, + ))), + ..Default::default() + })); + }); + + Some(guard) } #[actix_web::main] @@ -71,7 +95,24 @@ async fn main() -> Result<()> { "{lvl},reqwest=warn,hyper=warn,hyper_util=warn,h2=warn,rustls=warn,tokio_util=warn", lvl = config.log_level ); - env_logger::init_from_env(env_logger::Env::new().default_filter_or(log_filter)); + + let env_logger_logger = env_logger::Builder::from_env( + env_logger::Env::new().default_filter_or(log_filter), + ) + .build(); + let max_level = env_logger_logger.filter(); + let sentry_logger = sentry::integrations::log::SentryLogger::with_dest(env_logger_logger) + .filter(|md| match md.level() { + log::Level::Error => sentry::integrations::log::LogFilter::Exception, + log::Level::Warn => { + sentry::integrations::log::LogFilter::Log + | sentry::integrations::log::LogFilter::Breadcrumb + } + log::Level::Info => sentry::integrations::log::LogFilter::Breadcrumb, + log::Level::Debug | log::Level::Trace => sentry::integrations::log::LogFilter::Ignore, + }); + log::set_boxed_logger(Box::new(sentry_logger)).expect("Failed to set logger"); + log::set_max_level(max_level); if _sentry_guard.is_some() { log::info!(target: "Websocket", "Sentry error reporting enabled"); @@ -125,7 +166,7 @@ async fn main() -> Result<()> { .wrap(Governor::new(&governor_conf)) .wrap(Logger::default()) .wrap(cors) - .wrap(sentry_actix::Sentry::new()) + .wrap(sentry_actix::Sentry::with_transaction()) .app_data(session_manager.clone()) .app_data(server_config_for_app) .service(index) From 774d8591a9d4055f5cdf8fa14f6580249e31ff33 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 11:35:06 +0300 Subject: [PATCH 07/23] Web: Forward logs and traces to Sentry with privacy guarantees MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New SentryLoggerMonitor bridges ngx-logger output to Sentry: error/fatal → captureException (Issues), warn → log + breadcrumb (Logs page), info → breadcrumb only (context for the next error, not on Logs page) - Monitor registered via app initializer when Sentry is enabled - main.ts: enableLogs + tracesSampleRate read from environment; browserTracingIntegration added explicitly (not in default set); initialScope sets user.ip_address = 127.0.0.1 to suppress server-side geo enrichment on logs/transactions - Per-environment sentry.tracesSampleRate (0.1) and enableLogs (true) in environment.ts / environment.docker-dev.ts / environment.prod.ts --- client/web/src/app/app.config.ts | 11 +++- .../monitoring/sentry-logger-monitor.ts | 54 +++++++++++++++++++ .../environments/environment.docker-dev.ts | 2 + .../web/src/environments/environment.prod.ts | 2 + client/web/src/environments/environment.ts | 2 + client/web/src/main.ts | 10 +++- client/web/tsconfig.json | 3 +- 7 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts diff --git a/client/web/src/app/app.config.ts b/client/web/src/app/app.config.ts index 7706f595..2022bc1a 100644 --- a/client/web/src/app/app.config.ts +++ b/client/web/src/app/app.config.ts @@ -17,10 +17,11 @@ import { InMemoryTranslateLoader } from './core/i18n/translate-loader'; import { ThemeService } from './core/services/ui/theme.service'; import { LanguageService } from './core/services/ui/language.service'; import { provideHttpClient, withFetch } from '@angular/common/http'; -import { LoggerModule } from 'ngx-logger'; +import { LoggerModule, NGXLogger } from 'ngx-logger'; import { environment } from '../environments/environment'; import { DatePipe } from '@angular/common'; import { provideHotToastConfig } from '@ngxpert/hot-toast'; +import { SentryLoggerMonitor } from './core/services/monitoring/sentry-logger-monitor'; // Theme initialization function export function initializeTheme(themeService: ThemeService): () => Promise { @@ -85,6 +86,14 @@ export const appConfig: ApplicationConfig = { }) ), DatePipe, + // Forward ngx-logger output to Sentry (when Sentry is initialized). + // Registered as an app initializer so it's wired up before any logs flow. + provideAppInitializer(() => { + if (environment.sentry?.enabled && environment.sentry.dsn) { + const logger = inject(NGXLogger); + logger.registerMonitor(new SentryLoggerMonitor()); + } + }), // Initialize theme on app startup using app initializer provideAppInitializer(() => { const themeService = inject(ThemeService); diff --git a/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts b/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts new file mode 100644 index 00000000..873472b9 --- /dev/null +++ b/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts @@ -0,0 +1,54 @@ +import * as Sentry from '@sentry/angular'; +import { INGXLoggerMetadata, INGXLoggerMonitor, NgxLoggerLevel } from 'ngx-logger'; + +export class SentryLoggerMonitor implements INGXLoggerMonitor { + onLog(log: INGXLoggerMetadata): void { + if (!log) { + return; + } + const message = formatMessage(log); + + switch (log.level) { + case NgxLoggerLevel.FATAL: + case NgxLoggerLevel.ERROR: { + const err = pickError(log) ?? new Error(message); + Sentry.captureException(err, { level: 'error' }); + break; + } + case NgxLoggerLevel.WARN: { + Sentry.logger.warn(message); + Sentry.addBreadcrumb({ category: 'log', level: 'warning', message }); + break; + } + case NgxLoggerLevel.INFO: { + Sentry.addBreadcrumb({ category: 'log', level: 'info', message }); + break; + } + default: + // LOG / DEBUG / TRACE + break; + } + } +} + +function formatMessage(log: INGXLoggerMetadata): string { + const parts: string[] = []; + if (log.fileName) { + parts.push(log.fileName); + } + parts.push(typeof log.message === 'string' ? log.message : JSON.stringify(log.message)); + return parts.join(' :: '); +} + +function pickError(log: INGXLoggerMetadata): Error | undefined { + const additional = log.additional; + if (!Array.isArray(additional)) { + return undefined; + } + for (const value of additional) { + if (value instanceof Error) { + return value; + } + } + return undefined; +} diff --git a/client/web/src/environments/environment.docker-dev.ts b/client/web/src/environments/environment.docker-dev.ts index ab131c47..fbd61e77 100644 --- a/client/web/src/environments/environment.docker-dev.ts +++ b/client/web/src/environments/environment.docker-dev.ts @@ -11,5 +11,7 @@ export const environment = { enabled: false, dsn: '', environment: 'docker-dev', + tracesSampleRate: 0.1, + enableLogs: true, }, }; diff --git a/client/web/src/environments/environment.prod.ts b/client/web/src/environments/environment.prod.ts index ff694c99..7dad079b 100644 --- a/client/web/src/environments/environment.prod.ts +++ b/client/web/src/environments/environment.prod.ts @@ -11,5 +11,7 @@ export const environment = { enabled: true, dsn: '', environment: 'production', + tracesSampleRate: 0.1, + enableLogs: true, }, }; diff --git a/client/web/src/environments/environment.ts b/client/web/src/environments/environment.ts index e34fe4e8..9700d573 100644 --- a/client/web/src/environments/environment.ts +++ b/client/web/src/environments/environment.ts @@ -11,5 +11,7 @@ export const environment = { enabled: false, dsn: '', environment: 'development', + tracesSampleRate: 0.1, + enableLogs: true, }, }; diff --git a/client/web/src/main.ts b/client/web/src/main.ts index ea623c36..f8d1b4e4 100644 --- a/client/web/src/main.ts +++ b/client/web/src/main.ts @@ -5,18 +5,24 @@ import { AppComponent } from './app/app.component'; import { TranslateService } from '@ngx-translate/core'; import { LANGUAGE_PREFERENCE_KEY } from './app/utils/constants'; import { environment } from './environments/environment'; +import { name as pkgName, version as pkgVersion } from '../package.json'; // Initialize Sentry before bootstrapping the app so it can capture errors if (environment.sentry?.enabled && environment.sentry.dsn) { Sentry.init({ dsn: environment.sentry.dsn, environment: environment.sentry.environment, - release: 'web@0.17.0', + release: `${pkgName}@${pkgVersion}`, sendDefaultPii: false, maxBreadcrumbs: 50, - tracesSampleRate: 0, + tracesSampleRate: environment.sentry.tracesSampleRate ?? 0, + enableLogs: environment.sentry.enableLogs ?? false, replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 0, + integrations: [Sentry.browserTracingIntegration()], + initialScope: { + user: { ip_address: '127.0.0.1' }, + }, beforeSend(event) { // Strip user-identifying data before the event leaves the browser. event.user = { ip_address: '127.0.0.1' }; diff --git a/client/web/tsconfig.json b/client/web/tsconfig.json index 4df1d439..7564c21a 100644 --- a/client/web/tsconfig.json +++ b/client/web/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src/**/*.ts", "src/**/*.html"], + "include": ["src/**/*.ts", "src/**/*.html", "package.json"], "compileOnSave": false, "compilerOptions": { "outDir": "./dist/out-tsc", @@ -10,6 +10,7 @@ "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "esModuleInterop": true, + "resolveJsonModule": true, "sourceMap": true, "declaration": false, "moduleResolution": "bundler", From 0c54ca7e74735e9149b5ca61c2af1e87110110f7 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 13:39:14 +0300 Subject: [PATCH 08/23] Web: Enhance observability with Sentry spans - Integrate Sentry spans to track WebRTC connections and file uploads. - Add tracing for room management operations. --- .../communication/webrtc-signaling.service.ts | 114 ++++++++++++++++++ .../file-management/file-upload.service.ts | 16 +++ .../services/room-management/room.service.ts | 32 +++-- 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts index 10a75404..b931dd07 100644 --- a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts +++ b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts @@ -1,4 +1,5 @@ import { Injectable, inject } from '@angular/core'; +import * as Sentry from '@sentry/angular'; import { WebSocketConnectionService } from './websocket-connection.service'; import { UserService } from '../user-management/user.service'; import { @@ -45,6 +46,9 @@ export class WebRTCSignalingService { private stateMismatchTimeouts = new Map>(); private collectedCandidates = new Map(); + private activeConnectSpans = new Map(); + private connectAttemptCounts = new Map(); + constructor() { this.initializeSignalMessageHandler(); this.communicationService.dataChannelClosed$.subscribe((targetUser) => { @@ -71,11 +75,81 @@ export class WebRTCSignalingService { * @param targetUser The user to connect with */ public initiateConnection(targetUser: string): void { + let span = this.activeConnectSpans.get(targetUser); + if (!span) { + span = Sentry.startInactiveSpan({ name: 'webrtc.connect', op: 'webrtc.connect' }); + this.activeConnectSpans.set(targetUser, span); + this.connectAttemptCounts.set(targetUser, 1); + } else { + const next = (this.connectAttemptCounts.get(targetUser) ?? 1) + 1; + this.connectAttemptCounts.set(targetUser, next); + span.setAttribute('attempts', next); + } + this.initiateConnectionInner(targetUser, span); + } + + /** + * Marks the active webrtc.connect span as successful and ends it. + * Called when the peer connection AND data channel are both open. + */ + private finishConnectSpanAsSuccess(targetUser: string): void { + const span = this.activeConnectSpans.get(targetUser); + if (!span) return; + span.setAttribute('outcome', 'connected'); + span.setStatus({ code: 1, message: 'ok' }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); + } + + /** + * Marks the active webrtc.connect span as failed and attaches diagnostic + * candidate counts so a single trace explains *why* the peer-to-peer + * connection failed (e.g. no relay candidates → restrictive NAT). + */ + private finishConnectSpanAsFailed(targetUser: string, reason: string): void { + const span = this.activeConnectSpans.get(targetUser); + if (!span) return; + const peerConnection = this.peerConnections.get(targetUser); + const candidates = this.collectedCandidates.get(targetUser) || []; + const candidateTypeCounts = candidates.reduce>((acc, c) => { + const t = c.type || 'unknown'; + acc[t] = (acc[t] || 0) + 1; + return acc; + }, {}); + + span.setAttribute('outcome', 'failed'); + span.setAttribute('failure_reason', reason); + span.setAttribute('webrtc.peer_state', peerConnection?.connectionState ?? 'unknown'); + span.setAttribute('webrtc.ice_state', peerConnection?.iceConnectionState ?? 'unknown'); + span.setAttribute( + 'webrtc.has_relay', + candidates.some((c) => c.type === 'relay') + ); + span.setAttribute( + 'webrtc.has_srflx', + candidates.some((c) => c.type === 'srflx') + ); + span.setAttribute('webrtc.candidate_total', candidates.length); + for (const [type, count] of Object.entries(candidateTypeCounts)) { + span.setAttribute(`webrtc.candidate.${type}`, count); + } + span.setStatus({ code: 2, message: reason }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); + } + + private initiateConnectionInner(targetUser: string, span: Sentry.Span): void { if (targetUser === this.userService.user) { this.logger.warn( 'initiateConnection', `Preventing self-connection attempt to: "${targetUser}"` ); + span.setAttribute('outcome', 'self_connection_skipped'); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); return; } @@ -201,6 +275,7 @@ export class WebRTCSignalingService { this.logger.error('initiateConnection', `Connection initiation failed: ${error}`); this.toaster.error(this.translate.instant('CONNECTION_LOST')); this.connectionLocks.delete(targetUser); + this.finishConnectSpanAsFailed(targetUser, 'initiation_failed'); } } @@ -243,6 +318,15 @@ export class WebRTCSignalingService { * queued requests. Use when the peer is gone for good (left the room). */ public closeConnection(targetUser: string): void { + const span = this.activeConnectSpans.get(targetUser); + if (span) { + span.setAttribute('outcome', 'cancelled'); + span.setStatus({ code: 2, message: 'cancelled' }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); + } + this.closePeerConnection(targetUser, true); this.connectionLocks.delete(targetUser); @@ -302,6 +386,14 @@ export class WebRTCSignalingService { * Closes all peer connections */ public closeAllConnections(): void { + this.activeConnectSpans.forEach((span) => { + span.setAttribute('outcome', 'cancelled'); + span.setStatus({ code: 2, message: 'closeAll' }); + span.end(); + }); + this.activeConnectSpans.clear(); + this.connectAttemptCounts.clear(); + this.peerConnections.forEach((peerConnection) => { peerConnection.close(); }); @@ -438,6 +530,16 @@ export class WebRTCSignalingService { ); const hasRelay = candidates.some((c) => c.type === 'relay'); + Sentry.addBreadcrumb({ + category: 'webrtc.ice', + level: 'info', + message: 'ice gathering complete', + data: { + total: candidates.length, + types: candidateTypes, + has_relay: hasRelay, + }, + }); if (!hasRelay && candidates.length > 0) { this.logger.warn( 'ICE', @@ -463,6 +565,11 @@ export class WebRTCSignalingService { if (this.peerConnections.get(targetUser) !== peerConnection) return; const state = peerConnection.connectionState; + Sentry.addBreadcrumb({ + category: 'webrtc.peer', + level: state === 'failed' ? 'error' : 'info', + message: `peer connection state: ${state}`, + }); if (state === 'connected') { // Clear ICE gathering timeout when connection is established @@ -474,6 +581,7 @@ export class WebRTCSignalingService { if (this.communicationService.isConnected(targetUser)) { this.reconnectAttempts.delete(targetUser); this.logger.info('createPeerConnection', `Successfully connected to ${targetUser}`); + this.finishConnectSpanAsSuccess(targetUser); this.peerConnected$.next(targetUser); } else { this.logger.info( @@ -496,6 +604,11 @@ export class WebRTCSignalingService { if (this.peerConnections.get(targetUser) !== peerConnection) return; const iceState = peerConnection.iceConnectionState; + Sentry.addBreadcrumb({ + category: 'webrtc.ice', + level: iceState === 'failed' ? 'error' : 'info', + message: `ice connection state: ${iceState}`, + }); if (iceState === 'connected' || iceState === 'completed') { // Clear ICE gathering timeout when ICE connection is established @@ -585,6 +698,7 @@ export class WebRTCSignalingService { // Final diagnostic log this.logConnectionDiagnostics(targetUser); + this.finishConnectSpanAsFailed(targetUser, 'max_reconnects_exceeded'); if (this.wsService.isConnected()) { this.toaster.error( diff --git a/client/web/src/app/core/services/file-management/file-upload.service.ts b/client/web/src/app/core/services/file-management/file-upload.service.ts index d56f86b2..d793bc59 100644 --- a/client/web/src/app/core/services/file-management/file-upload.service.ts +++ b/client/web/src/app/core/services/file-management/file-upload.service.ts @@ -1,4 +1,5 @@ import { Injectable, inject } from '@angular/core'; +import * as Sentry from '@sentry/angular'; import { FileUpload, CHUNK_SIZE, @@ -436,6 +437,16 @@ export class FileUploadService extends FileTransferBaseService { * Each chunk contains embedded fileId, eliminating chunk mismatching. */ private async sendFileChunks(fileTransfer: FileUpload): Promise { + return Sentry.startSpan( + { name: 'file.transfer.send', op: 'file.transfer.send' }, + async (span) => { + span.setAttribute('file_size_bytes', fileTransfer.file.size); + return this.sendFileChunksInner(fileTransfer, span); + } + ); + } + + private async sendFileChunksInner(fileTransfer: FileUpload, span: Sentry.Span): Promise { const transferId = this.getOrCreateStatusKey(fileTransfer.targetUser, fileTransfer.fileId); if (this.processingQueues.get(transferId)) { this.logger.warn( @@ -555,6 +566,9 @@ export class FileUploadService extends FileTransferBaseService { this.consecutiveErrorCounts.set(transferId, errorCount + 1); if (errorCount >= this.maxConsecutiveErrors) { + span.setAttribute('outcome', 'aborted_max_errors'); + span.setAttribute('consecutive_errors', errorCount); + span.setStatus({ code: 2, message: 'max_consecutive_errors' }); await this.stopFileUpload(fileTransfer.targetUser, fileTransfer.fileId); break; } @@ -568,6 +582,8 @@ export class FileUploadService extends FileTransferBaseService { 'sendFileChunks', `Completed ${fileTransfer.fileId} to ${fileTransfer.targetUser}` ); + span.setAttribute('outcome', 'completed'); + span.setStatus({ code: 1, message: 'ok' }); fileTransfer.progress = 100; const key = this.getOrCreateStatusKey(fileTransfer.targetUser, fileTransfer.fileId); diff --git a/client/web/src/app/core/services/room-management/room.service.ts b/client/web/src/app/core/services/room-management/room.service.ts index 7d505a0e..d2c768a1 100644 --- a/client/web/src/app/core/services/room-management/room.service.ts +++ b/client/web/src/app/core/services/room-management/room.service.ts @@ -1,5 +1,6 @@ import { Injectable, NgZone, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; +import * as Sentry from '@sentry/angular'; import { WebSocketConnectionService } from '../communication/websocket-connection.service'; import { NGXLogger } from 'ngx-logger'; import { IRoomService } from '../../interfaces/room.interface'; @@ -58,19 +59,24 @@ export class RoomService implements IRoomService { } public joinRoom(room: string): void { - const sanitizedRoom = room - .replace(/[^a-zA-Z0-9\-_ ]/g, '') - .trim() - .substring(0, 64); - if (!sanitizedRoom) { - this.logger.warn('joinRoom', `Room name is empty after sanitization: ${room}`); - return; - } - if (sanitizedRoom !== this.currentRoom) { - this.wsService.send(`[UserCommand] /join ${sanitizedRoom}`); - } else { - this.logger.warn('joinRoom', `Already in room: ${room}`); - } + Sentry.startSpan({ name: 'session.join', op: 'session.join' }, (span) => { + const sanitizedRoom = room + .replace(/[^a-zA-Z0-9\-_ ]/g, '') + .trim() + .substring(0, 64); + if (!sanitizedRoom) { + this.logger.warn('joinRoom', `Room name is empty after sanitization: ${room}`); + span.setStatus({ code: 2, message: 'invalid_room_name' }); + return; + } + if (sanitizedRoom !== this.currentRoom) { + this.wsService.send(`[UserCommand] /join ${sanitizedRoom}`); + span.setAttribute('outcome', 'join_requested'); + } else { + this.logger.warn('joinRoom', `Already in room: ${room}`); + span.setAttribute('outcome', 'already_in_room'); + } + }); } /** From ff8be5a9befed3943fe2b1a42d874cd58573ab9e Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 13:57:50 +0300 Subject: [PATCH 09/23] Web: Boost Sentry observability for file transfers - Enhances error monitoring by adding Sentry breadcrumbs in WebRTC data channels. - Introduces Sentry spans to track file download and upload events, improving traceability. - Streamlines Sentry data for ICE gathering by reducing excessive relay information. --- .../webrtc-communication.service.ts | 14 +++++ .../communication/webrtc-signaling.service.ts | 6 +- .../file-management/file-download.service.ts | 58 ++++++++++++++++++- .../file-management/file-upload.service.ts | 1 - 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/client/web/src/app/core/services/communication/webrtc-communication.service.ts b/client/web/src/app/core/services/communication/webrtc-communication.service.ts index 43728556..871d9d55 100644 --- a/client/web/src/app/core/services/communication/webrtc-communication.service.ts +++ b/client/web/src/app/core/services/communication/webrtc-communication.service.ts @@ -1,4 +1,5 @@ import { Injectable, NgZone, PLATFORM_ID, inject } from '@angular/core'; +import * as Sentry from '@sentry/angular'; import { BehaviorSubject, Subject } from 'rxjs'; import { BUFFERED_AMOUNT_LOW_THRESHOLD, @@ -134,6 +135,7 @@ export class WebRTCCommunicationService { // Ignore events from stale data channels that have been replaced if (this.dataChannels.get(targetUser) !== channel) return; + let errorDetail = 'unknown'; if ('error' in ev) { const rtcErrorEvent = ev as RTCErrorEvent; const error = rtcErrorEvent.error; @@ -141,6 +143,7 @@ export class WebRTCCommunicationService { (error && typeof error.message === 'string' && error.message) || (typeof error === 'object' ? JSON.stringify(error) : String(error)) || 'Unknown RTCErrorEvent'; + errorDetail = errorMsg; this.logger.error('setupDataChannel', `Data Channel Error with ${targetUser}: ${errorMsg}`); } else { this.logger.error( @@ -148,6 +151,12 @@ export class WebRTCCommunicationService { `Data Channel Error with ${targetUser}: ${JSON.stringify(ev)}` ); } + Sentry.addBreadcrumb({ + category: 'webrtc.datachannel', + level: 'error', + message: 'data channel error', + data: { ready_state: channel.readyState, error: errorDetail }, + }); this.dataChannelClosed$.next(targetUser); }; @@ -453,6 +462,11 @@ export class WebRTCCommunicationService { 'handleDataChannelMessage', `Failed to decode chunk from ${targetUser}, size: ${data.byteLength}` ); + Sentry.addBreadcrumb({ + category: 'webrtc.datachannel', + level: 'error', + message: 'chunk decode failed', + }); } } else { this.logger.warn( diff --git a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts index b931dd07..585c86cd 100644 --- a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts +++ b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts @@ -534,11 +534,7 @@ export class WebRTCSignalingService { category: 'webrtc.ice', level: 'info', message: 'ice gathering complete', - data: { - total: candidates.length, - types: candidateTypes, - has_relay: hasRelay, - }, + data: { types: candidateTypes }, }); if (!hasRelay && candidates.length > 0) { this.logger.warn( diff --git a/client/web/src/app/core/services/file-management/file-download.service.ts b/client/web/src/app/core/services/file-management/file-download.service.ts index 410649d3..994ed8ce 100644 --- a/client/web/src/app/core/services/file-management/file-download.service.ts +++ b/client/web/src/app/core/services/file-management/file-download.service.ts @@ -1,4 +1,5 @@ import { Injectable, inject } from '@angular/core'; +import * as Sentry from '@sentry/angular'; import { FILE_TRANSFER_MESSAGE_TYPES, FileDownload, @@ -19,11 +20,50 @@ import { export class FileDownloadService extends FileTransferBaseService { private previewService = inject(PreviewService); + private activeReceiveSpans = new Map(); + // =============== Constructor =============== constructor() { super(); } + private receiveSpanKey(fromUser: string, fileId: string): string { + return `${fromUser}:${fileId}`; + } + + private startReceiveSpan(fromUser: string, fileId: string, fileSize: number): void { + const key = this.receiveSpanKey(fromUser, fileId); + if (this.activeReceiveSpans.has(key)) return; + const span = Sentry.startInactiveSpan({ + name: 'file.transfer.receive', + op: 'file.transfer.receive', + }); + span.setAttribute('file_size_bytes', fileSize); + this.activeReceiveSpans.set(key, span); + } + + private finishReceiveSpan( + fromUser: string, + fileId: string, + outcome: 'completed' | 'crc_failed' | 'missing_chunks' | 'hash_mismatch' | 'cancelled', + extra?: Record + ): void { + const key = this.receiveSpanKey(fromUser, fileId); + const span = this.activeReceiveSpans.get(key); + if (!span) return; + span.setAttribute('outcome', outcome); + if (extra) { + for (const [k, v] of Object.entries(extra)) { + span.setAttribute(k, v); + } + } + span.setStatus( + outcome === 'completed' ? { code: 1, message: 'ok' } : { code: 2, message: outcome } + ); + span.end(); + this.activeReceiveSpans.delete(key); + } + // =============== Data Handling Methods =============== /** * Handles incoming file data chunks and assembles the file. @@ -76,12 +116,18 @@ export class FileDownloadService extends FileTransferBaseService { totalChunks, }) ); + this.finishReceiveSpan(fromUser, fileId, 'crc_failed', { + chunk_index: chunkIndex, + total_chunks: totalChunks, + bytes_received: fileDownload.receivedSize, + }); return; } - // Initialize totalChunks if not set + // Initialize totalChunks if (fileDownload.totalChunks === 0) { fileDownload.totalChunks = totalChunks; + this.startReceiveSpan(fromUser, fileId, fileDownload.fileSize); } // Check for duplicate chunk @@ -155,6 +201,10 @@ export class FileDownloadService extends FileTransferBaseService { ); this.toaster.error(this.translate.instant('FILE_INCOMPLETE_ERROR', { count: missingChunks })); orderedChunks.length = 0; // Clear to free memory + this.finishReceiveSpan(fromUser, fileDownload.fileId, 'missing_chunks', { + missing_chunks: missingChunks, + total_chunks: fileDownload.totalChunks, + }); await this.cleanupAfterDownload(fileDownload.fromUser, fileDownload.fileId); return; } @@ -183,6 +233,7 @@ export class FileDownloadService extends FileTransferBaseService { this.logger.error('assembleAndDownloadFile', `Hash mismatch for ${fileDownload.fileId}!`); this.toaster.error(this.translate.instant('CHUNK_INTEGRITY_ERROR')); orderedChunks.length = 0; // Clear to free memory + this.finishReceiveSpan(fromUser, fileDownload.fileId, 'hash_mismatch'); await this.cleanupAfterDownload(fileDownload.fromUser, fileDownload.fileId); return; // Abort - don't download corrupted file } @@ -254,6 +305,8 @@ export class FileDownloadService extends FileTransferBaseService { this.toaster.success(this.translate.instant('FILE_DOWNLOAD_COMPLETED', { fileName })); + this.finishReceiveSpan(fromUser, fileDownload.fileId, 'completed'); + // Cleanup userMap.delete(fileDownload.fileId); if (userMap.size === 0) { @@ -286,6 +339,7 @@ export class FileDownloadService extends FileTransferBaseService { public async cancelFileDownload(fromUser: string, fileId: string): Promise { const userMap = await this.getIncomingFileTransfers(fromUser); if (userMap?.has(fileId)) { + this.finishReceiveSpan(fromUser, fileId, 'cancelled', { cancelled_by: 'receiver' }); userMap.delete(fileId); const key = this.getOrCreateStatusKey(fromUser, fileId); await this.deleteFileTransferStatus(key); @@ -311,6 +365,8 @@ export class FileDownloadService extends FileTransferBaseService { `File upload from ${fromUser} (fileId=${fileId}) was cancelled` ); + this.finishReceiveSpan(fromUser, fileId, 'cancelled', { cancelled_by: 'sender' }); + const userMap = await this.getIncomingFileTransfers(fromUser); if (userMap) { userMap.delete(fileId); diff --git a/client/web/src/app/core/services/file-management/file-upload.service.ts b/client/web/src/app/core/services/file-management/file-upload.service.ts index d793bc59..ece3bbf4 100644 --- a/client/web/src/app/core/services/file-management/file-upload.service.ts +++ b/client/web/src/app/core/services/file-management/file-upload.service.ts @@ -567,7 +567,6 @@ export class FileUploadService extends FileTransferBaseService { if (errorCount >= this.maxConsecutiveErrors) { span.setAttribute('outcome', 'aborted_max_errors'); - span.setAttribute('consecutive_errors', errorCount); span.setStatus({ code: 2, message: 'max_consecutive_errors' }); await this.stopFileUpload(fileTransfer.targetUser, fileTransfer.fileId); break; From 7a279ce3cad4238b02e8d77f74d6c8ce28cc603f Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 14:21:40 +0300 Subject: [PATCH 10/23] Docker: Enhance env var validation - Improve Docker environment variable error messages to prevent misconfiguration by providing clear validation prompts. - Fix lint for .env.*.example files. - Update documentation to clarify how environment variables impact build and runtime configurations. --- .env.development.example | 2 +- .env.production.example | 2 +- README.md | 2 +- docker-compose.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.development.example b/.env.development.example index 5d838f5d..2567bdb6 100644 --- a/.env.development.example +++ b/.env.development.example @@ -24,5 +24,5 @@ SERVER_NAME=127.0.0.1 # Leave SENTRY_ENABLED=false in dev unless you actively want to send events. # SENTRY_DSN must stay empty in this committed file — set it only in your # local .env.development (which is gitignored). -SENTRY_ENABLED=false SENTRY_DSN= +SENTRY_ENABLED=false \ No newline at end of file diff --git a/.env.production.example b/.env.production.example index 5e41c6af..9b835e57 100644 --- a/.env.production.example +++ b/.env.production.example @@ -24,5 +24,5 @@ SERVER_NAME=pastepoint.com # Sentry (error tracking) # Set SENTRY_DSN to the production server DSN. Without it, Sentry stays # disabled even if SENTRY_ENABLED=true. -SENTRY_ENABLED=true SENTRY_DSN= +SENTRY_ENABLED=true \ No newline at end of file diff --git a/README.md b/README.md index 5575a8d6..9335de76 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ PastePoint is a secure, feature-rich file-sharing service designed for local net - **Observability**: - Optional Sentry-based error tracking (EU-hosted, off by default in dev) - Privacy-tight defaults: no IPs, no geo, no request bodies, no user identifiers - - Toggle per-environment via `SENTRY_ENABLED` / `SENTRY_DSN` + - Toggle per-environment via `SENTRY_ENABLED` / `SENTRY_DSN` (server: runtime env vars; web: built into the bundle from `client/web/src/environments/environment.*.ts` at compile time) - **Cross-Platform Compatibility**: - Runs seamlessly on Linux, macOS, and Windows with Dockerized support diff --git a/docker-compose.yml b/docker-compose.yml index 37828cb2..d22e3d2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,7 +80,7 @@ services: dockerfile: client/web/Dockerfile target: nginx args: - NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG} + NPM_BUILD_CONFIG: ${NPM_BUILD_CONFIG:?NPM_BUILD_CONFIG is required (set it in your .env file)} container_name: pastepoint-nginx environment: SERVER_NAME: ${SERVER_NAME:?SERVER_NAME is required (set it in your .env file)} From 894ca2cc997a05519a3912bed0fb84070212f0f8 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 14:21:55 +0300 Subject: [PATCH 11/23] Server: Improve Sentry integration with conditionals - Use conditional middleware to enable Sentry only if it's configured. - Removed redundant log statements to streamline error reporting. --- server/src/main.rs | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/server/src/main.rs b/server/src/main.rs index 6bf7d275..5ad970d0 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,7 +1,11 @@ use actix_cors::Cors; use actix_governor::{Governor, GovernorConfigBuilder}; use actix_http::KeepAlive; -use actix_web::{App, HttpServer, middleware::Logger, web::Data}; +use actix_web::{ + App, HttpServer, + middleware::{Condition, Logger}, + web::Data, +}; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; use server::{ CORS_MAX_AGE, KEEP_ALIVE_INTERVAL, SentryConfig, ServerConfig, SessionStore, chat_ws, @@ -53,19 +57,6 @@ fn init_sentry(cfg: &SentryConfig) -> Option { req.data = None; req.query_string = None; } - log::info!( - target: "Sentry", - "Captured {} id={} level={:?} tx={:?} msg={:?}", - if event.transaction.is_some() && event.exception.values.is_empty() { - "transaction" - } else { - "event" - }, - event.event_id, - event.level, - event.transaction.as_deref().unwrap_or(""), - event.message.as_deref().unwrap_or("") - ); Some(event) })), ..Default::default() @@ -96,10 +87,8 @@ async fn main() -> Result<()> { lvl = config.log_level ); - let env_logger_logger = env_logger::Builder::from_env( - env_logger::Env::new().default_filter_or(log_filter), - ) - .build(); + let env_logger_logger = + env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter)).build(); let max_level = env_logger_logger.filter(); let sentry_logger = sentry::integrations::log::SentryLogger::with_dest(env_logger_logger) .filter(|md| match md.level() { @@ -152,6 +141,7 @@ async fn main() -> Result<()> { let session_manager = Data::new(SessionStore::default()); let server_config = Data::new(config.clone()); + let sentry_enabled = _sentry_guard.is_some(); HttpServer::new(move || { let server_config = server_config.clone(); @@ -166,7 +156,10 @@ async fn main() -> Result<()> { .wrap(Governor::new(&governor_conf)) .wrap(Logger::default()) .wrap(cors) - .wrap(sentry_actix::Sentry::with_transaction()) + .wrap(Condition::new( + sentry_enabled, + sentry_actix::Sentry::with_transaction(), + )) .app_data(session_manager.clone()) .app_data(server_config_for_app) .service(index) From 35c0f857a7eb1bc3f580b1f71d9967af7d623646 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 18:55:27 +0300 Subject: [PATCH 12/23] Nginx: Updates security headers to expand Sentry observability. - Auto-detects environment files for Docker operations. - Improves error handling when env files are missing. --- Makefile | 14 +++++++++++--- nginx/security/security_headers.conf | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index c65bc43f..3f7eaad6 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,11 @@ export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 +# Env file used by read-only targets (logs/down/stop). Auto-detects whichever +# file exists; prefers .env.development on dev machines that have both. +# Override explicitly: `make logs ENV_FILE=.env.production` +ENV_FILE ?= $(firstword $(wildcard .env.development .env.production)) + # Production environment (default) prod: @echo "Starting production environment..." @@ -25,17 +30,20 @@ dev: # Stop and remove PastePoint containers down: @echo "Stopping and removing PastePoint services..." - docker compose down + @test -n "$(ENV_FILE)" || (echo "Error: no .env.development or .env.production found." && exit 1) + docker compose --env-file $(ENV_FILE) down # Stop PastePoint containers without removing them stop: @echo "Stopping PastePoint services..." - docker compose stop + @test -n "$(ENV_FILE)" || (echo "Error: no .env.development or .env.production found." && exit 1) + docker compose --env-file $(ENV_FILE) stop # View logs logs: @echo "Viewing logs (Ctrl+C to exit)..." - docker compose logs -f + @test -n "$(ENV_FILE)" || (echo "Error: no .env.development or .env.production found." && exit 1) + docker compose --env-file $(ENV_FILE) logs -f # Generate certificates (if needed) certs: diff --git a/nginx/security/security_headers.conf b/nginx/security/security_headers.conf index 1938c58e..2b28d0d6 100644 --- a/nginx/security/security_headers.conf +++ b/nginx/security/security_headers.conf @@ -12,7 +12,7 @@ set $script_src "'self' 'unsafe-inline' 'wasm-unsafe-eval' https://cdn.jsdelivr. set $style_src "'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com"; set $img_src "'self' data: https://cdn.jsdelivr.net blob:"; set $font_src "'self' https://cdn.jsdelivr.net https://fonts.gstatic.com"; -set $connect_src "'self' wss://${SERVER_NAME}:* https://${SERVER_NAME}:* ws://${SERVER_NAME}:* https://cdn.jsdelivr.net"; +set $connect_src "'self' wss://${SERVER_NAME}:* https://${SERVER_NAME}:* ws://${SERVER_NAME}:* https://cdn.jsdelivr.net https://*.ingest.sentry.io https://*.ingest.de.sentry.io https://*.ingest.us.sentry.io"; set $frame_ancestors "'none'"; set $form_action "'self'"; set $base_uri "'self'"; From 04af2423b553b24e6234706506409999a88bca3e Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 18:55:59 +0300 Subject: [PATCH 13/23] Server: Adjust Sentry to Ignore WebSocket Traces - Reduces noise in Sentry by excluding tracing for "/ws" endpoint requests. --- server/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main.rs b/server/src/main.rs index 5ad970d0..53e55309 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -36,6 +36,7 @@ fn init_sentry(cfg: &SentryConfig) -> Option { server_name: Some(Cow::Borrowed("pastepoint-server")), traces_sampler: Some(Arc::new(move |ctx| match ctx.name() { "signaling.relay" => 0.01, + name if name.starts_with("GET /ws") => 0.0, _ => global_traces_rate, })), send_default_pii: false, From 5ce80c9d2469000595b7fa81a0873039614952fe Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 18:57:10 +0300 Subject: [PATCH 14/23] Web: Improve logging and error handling for observability - Switch file download log from error to debug level to reduce noise. - Add logger info for reconnect issues when page visibility changes. - Enhance WebSocket error handling to provide detailed info for debugging. - Boost Sentry observability with enriched error context and additional log data. --- .../communication/websocket-connection.service.ts | 6 +++--- .../file-management/file-download.service.ts | 4 ++-- .../services/monitoring/sentry-logger-monitor.ts | 13 ++++++++++++- client/web/src/app/features/chat/chat.component.ts | 10 +++++++--- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/client/web/src/app/core/services/communication/websocket-connection.service.ts b/client/web/src/app/core/services/communication/websocket-connection.service.ts index fecb1dcb..752219f3 100644 --- a/client/web/src/app/core/services/communication/websocket-connection.service.ts +++ b/client/web/src/app/core/services/communication/websocket-connection.service.ts @@ -278,13 +278,13 @@ export class WebSocketConnectionService implements OnDestroy { } }; - socket.onerror = (error) => { + socket.onerror = (_event) => { if (socket !== this.socket) return; - this.logger.error('connect', 'WebSocket error: ' + error); + this.logger.warn('connect', 'WebSocket connection error (will attempt reconnect)'); this.isConnecting = false; this.stopKeepAlive(); this.socket = undefined; - settleReject(error); + settleReject(new Error('WebSocket connection error')); }; }); } diff --git a/client/web/src/app/core/services/file-management/file-download.service.ts b/client/web/src/app/core/services/file-management/file-download.service.ts index 994ed8ce..a65619b8 100644 --- a/client/web/src/app/core/services/file-management/file-download.service.ts +++ b/client/web/src/app/core/services/file-management/file-download.service.ts @@ -158,9 +158,9 @@ export class FileDownloadService extends FileTransferBaseService { this.logger.info('handleDataChunk', `All chunks received for fileId=${fileId}`); await this.assembleAndDownloadFile(fileDownload, userMap, fromUser); } else { - this.logger.error( + this.logger.debug( 'handleDataChunk', - `File ${fileId.substring(0, 8)}... not fully received: ${fileDownload.receivedChunks.size}/${totalChunks} chunks` + `File ${fileId.substring(0, 8)}... in progress: ${fileDownload.receivedChunks.size}/${totalChunks} chunks` ); } } diff --git a/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts b/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts index 873472b9..44557209 100644 --- a/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts +++ b/client/web/src/app/core/services/monitoring/sentry-logger-monitor.ts @@ -12,7 +12,10 @@ export class SentryLoggerMonitor implements INGXLoggerMonitor { case NgxLoggerLevel.FATAL: case NgxLoggerLevel.ERROR: { const err = pickError(log) ?? new Error(message); - Sentry.captureException(err, { level: 'error' }); + Sentry.captureException(err, { + level: 'error', + extra: { logger: message }, + }); break; } case NgxLoggerLevel.WARN: { @@ -37,6 +40,14 @@ function formatMessage(log: INGXLoggerMetadata): string { parts.push(log.fileName); } parts.push(typeof log.message === 'string' ? log.message : JSON.stringify(log.message)); + if (Array.isArray(log.additional)) { + for (const value of log.additional) { + if (typeof value === 'string') { + parts.push(value); + break; + } + } + } return parts.join(' :: '); } diff --git a/client/web/src/app/features/chat/chat.component.ts b/client/web/src/app/features/chat/chat.component.ts index f942f937..95a3b4de 100644 --- a/client/web/src/app/features/chat/chat.component.ts +++ b/client/web/src/app/features/chat/chat.component.ts @@ -168,7 +168,9 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewInit { !this.wsConnectionService.isConnected() ) { this.logger.info('visibilitychange', 'Page visible, reconnecting if needed'); - void this.connect(this.SessionCode); + this.connect(this.SessionCode).catch((err: unknown) => { + this.logger.warn('visibilitychange', `Reconnect after visibility change failed: ${err}`); + }); } }; private beforeUnloadHandler = () => { @@ -834,8 +836,10 @@ export class ChatComponent implements OnInit, OnDestroy, AfterViewInit { } }) .catch((error: unknown) => { - this.logger.error('connect', `WebSocket connection failed: ${error}`); - throw error; + const err = + error instanceof Error ? error : new Error(`WebSocket connection failed: ${error}`); + this.logger.error('connect', err.message, err); + throw err; }); } From 2bf25b3278227d1e6dea8d892e901e16d1c3d525 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:00:58 +0300 Subject: [PATCH 15/23] Web: Add Sentry tracing for observability - Integrated Sentry with WebSocket connections and file transfers. - Enhanced performance insights with new trace data points. - Improved error tracking and diagnostics through Sentry spans. --- .../websocket-connection.service.ts | 17 +++++++++++++++++ .../file-management/file-download.service.ts | 13 ++++++++----- .../file-management/file-upload.service.ts | 8 ++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/client/web/src/app/core/services/communication/websocket-connection.service.ts b/client/web/src/app/core/services/communication/websocket-connection.service.ts index 752219f3..32c22f87 100644 --- a/client/web/src/app/core/services/communication/websocket-connection.service.ts +++ b/client/web/src/app/core/services/communication/websocket-connection.service.ts @@ -1,5 +1,7 @@ import { Injectable, PLATFORM_ID, OnDestroy, inject } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; +import * as Sentry from '@sentry/angular'; +import { startNewTrace } from '@sentry/core'; import { environment } from '../../../../environments/environment'; import { Router } from '@angular/router'; import { NGXLogger } from 'ngx-logger'; @@ -173,6 +175,16 @@ export class WebSocketConnectionService implements OnDestroy { this.sessionCode = code; const wsUri = `${this.webSocketProto}://${this.host}/ws${code ? `/${code}` : ''}`; + + let connectSpan: Sentry.Span | undefined; + startNewTrace(() => { + connectSpan = Sentry.startInactiveSpan({ + name: 'ws.connect', + op: 'ws.connect', + attributes: { 'ws.url': wsUri, 'ws.session_code': code ?? 'public' }, + }); + }); + return new Promise((resolve, reject) => { this.logger.info('connect', `Connecting to WebSocket at ${wsUri}`); const socket = new WebSocket(wsUri); @@ -185,11 +197,16 @@ export class WebSocketConnectionService implements OnDestroy { const settleResolve = () => { if (settled) return; settled = true; + connectSpan?.setStatus({ code: 1, message: 'ok' }); + connectSpan?.end(); resolve(); }; const settleReject = (err: unknown) => { if (settled) return; settled = true; + const msg = err instanceof Error ? err.message : String(err); + connectSpan?.setStatus({ code: 2, message: msg }); + connectSpan?.end(); reject(err); }; diff --git a/client/web/src/app/core/services/file-management/file-download.service.ts b/client/web/src/app/core/services/file-management/file-download.service.ts index a65619b8..a2ebeb33 100644 --- a/client/web/src/app/core/services/file-management/file-download.service.ts +++ b/client/web/src/app/core/services/file-management/file-download.service.ts @@ -1,5 +1,6 @@ import { Injectable, inject } from '@angular/core'; import * as Sentry from '@sentry/angular'; +import { startNewTrace } from '@sentry/core'; import { FILE_TRANSFER_MESSAGE_TYPES, FileDownload, @@ -34,12 +35,14 @@ export class FileDownloadService extends FileTransferBaseService { private startReceiveSpan(fromUser: string, fileId: string, fileSize: number): void { const key = this.receiveSpanKey(fromUser, fileId); if (this.activeReceiveSpans.has(key)) return; - const span = Sentry.startInactiveSpan({ - name: 'file.transfer.receive', - op: 'file.transfer.receive', + startNewTrace(() => { + const span = Sentry.startInactiveSpan({ + name: 'file.transfer.receive', + op: 'file.transfer.receive', + }); + span.setAttribute('file_size_bytes', fileSize); + this.activeReceiveSpans.set(key, span); }); - span.setAttribute('file_size_bytes', fileSize); - this.activeReceiveSpans.set(key, span); } private finishReceiveSpan( diff --git a/client/web/src/app/core/services/file-management/file-upload.service.ts b/client/web/src/app/core/services/file-management/file-upload.service.ts index ece3bbf4..fe606430 100644 --- a/client/web/src/app/core/services/file-management/file-upload.service.ts +++ b/client/web/src/app/core/services/file-management/file-upload.service.ts @@ -1,5 +1,6 @@ import { Injectable, inject } from '@angular/core'; import * as Sentry from '@sentry/angular'; +import { startNewTrace } from '@sentry/core'; import { FileUpload, CHUNK_SIZE, @@ -437,12 +438,11 @@ export class FileUploadService extends FileTransferBaseService { * Each chunk contains embedded fileId, eliminating chunk mismatching. */ private async sendFileChunks(fileTransfer: FileUpload): Promise { - return Sentry.startSpan( - { name: 'file.transfer.send', op: 'file.transfer.send' }, - async (span) => { + return startNewTrace(() => + Sentry.startSpan({ name: 'file.transfer.send', op: 'file.transfer.send' }, async (span) => { span.setAttribute('file_size_bytes', fileTransfer.file.size); return this.sendFileChunksInner(fileTransfer, span); - } + }) ); } From e0bb25906cc1ade9414155ec06c7f8a4c8cd0370 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:01:24 +0300 Subject: [PATCH 16/23] Web: Optimize Sentry initialization for SSR - Ensure Sentry doesn't initialize during server-side rendering. - Reduce noise in traces by filtering unnecessary spans. - Improve browser tracing with refined integration settings. --- client/web/src/main.ts | 45 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/client/web/src/main.ts b/client/web/src/main.ts index f8d1b4e4..7ee9b08e 100644 --- a/client/web/src/main.ts +++ b/client/web/src/main.ts @@ -7,8 +7,9 @@ import { LANGUAGE_PREFERENCE_KEY } from './app/utils/constants'; import { environment } from './environments/environment'; import { name as pkgName, version as pkgVersion } from '../package.json'; -// Initialize Sentry before bootstrapping the app so it can capture errors -if (environment.sentry?.enabled && environment.sentry.dsn) { +// Initialize Sentry before bootstrapping the app so it can capture errors. +// Skip during SSR/prerender (no `window`) +if (typeof window !== 'undefined' && environment.sentry?.enabled && environment.sentry.dsn) { Sentry.init({ dsn: environment.sentry.dsn, environment: environment.sentry.environment, @@ -19,10 +20,48 @@ if (environment.sentry?.enabled && environment.sentry.dsn) { enableLogs: environment.sentry.enableLogs ?? false, replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 0, - integrations: [Sentry.browserTracingIntegration()], + integrations: [ + Sentry.browserTracingIntegration({ + instrumentPageLoad: false, + ignoreResourceSpans: [ + 'resource.img', + 'resource.script', + 'resource.css', + 'resource.other', + 'resource.link', + ], + ignorePerformanceApiSpans: [/.*/], + enableLongAnimationFrame: false, + enableLongTask: false, + }), + ], initialScope: { user: { ip_address: '127.0.0.1' }, }, + beforeSendTransaction(event) { + // Strip browser navigation-timing child spans and paint entries — they + // add noise without actionable signal for this app. + const IGNORED_OPS = new Set([ + 'browser.DNS', + 'browser.TLS/SSL', + 'browser.connect', + 'browser.cache', + 'browser.request', + 'browser.response', + 'browser.loadEvent', + 'browser.unloadEvent', + 'browser.domContentLoadedEvent', + 'paint', + ]); + if (event.spans) { + event.spans = event.spans.filter( + (span) => + !IGNORED_OPS.has(span.op ?? '') && + !(span.op === 'http.client' && span.description?.includes('.js.map')) + ); + } + return event; + }, beforeSend(event) { // Strip user-identifying data before the event leaves the browser. event.user = { ip_address: '127.0.0.1' }; From 927d764776e7c3a3203c28001e66f9ae582013d0 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:06:19 +0300 Subject: [PATCH 17/23] Web: Enhance room tracing observability - Introduce inactive spans for improved trace clarity. - Reset pending traces on cancellation or supersession. - Simplify room joining logic with sanitation and validation. - Improve Sentry's integration for room activities. --- .../services/room-management/room.service.ts | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/client/web/src/app/core/services/room-management/room.service.ts b/client/web/src/app/core/services/room-management/room.service.ts index d2c768a1..0eb857fd 100644 --- a/client/web/src/app/core/services/room-management/room.service.ts +++ b/client/web/src/app/core/services/room-management/room.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import * as Sentry from '@sentry/angular'; +import { startNewTrace } from '@sentry/core'; import { WebSocketConnectionService } from '../communication/websocket-connection.service'; import { NGXLogger } from 'ngx-logger'; import { IRoomService } from '../../interfaces/room.interface'; @@ -23,6 +24,8 @@ export class RoomService implements IRoomService { public members$ = new BehaviorSubject([]); public currentRoom = 'main'; + private pendingJoinSpan: Sentry.Span | null = null; + /** * ========================================================== * CONSTRUCTOR @@ -51,6 +54,11 @@ export class RoomService implements IRoomService { * replay the previous session's members/rooms into the new view. */ public reset(): void { + if (this.pendingJoinSpan) { + this.pendingJoinSpan.setStatus({ code: 2, message: 'cancelled' }); + this.pendingJoinSpan.end(); + this.pendingJoinSpan = null; + } this.ngZone.run(() => { this.rooms$.next([]); this.members$.next([]); @@ -59,24 +67,35 @@ export class RoomService implements IRoomService { } public joinRoom(room: string): void { - Sentry.startSpan({ name: 'session.join', op: 'session.join' }, (span) => { - const sanitizedRoom = room - .replace(/[^a-zA-Z0-9\-_ ]/g, '') - .trim() - .substring(0, 64); - if (!sanitizedRoom) { - this.logger.warn('joinRoom', `Room name is empty after sanitization: ${room}`); - span.setStatus({ code: 2, message: 'invalid_room_name' }); - return; - } - if (sanitizedRoom !== this.currentRoom) { - this.wsService.send(`[UserCommand] /join ${sanitizedRoom}`); - span.setAttribute('outcome', 'join_requested'); - } else { - this.logger.warn('joinRoom', `Already in room: ${room}`); - span.setAttribute('outcome', 'already_in_room'); - } + const sanitizedRoom = room + .replace(/[^a-zA-Z0-9\-_ ]/g, '') + .trim() + .substring(0, 64); + + if (!sanitizedRoom) { + this.logger.warn('joinRoom', `Room name is empty after sanitization: ${room}`); + return; + } + + if (sanitizedRoom === this.currentRoom) { + this.logger.warn('joinRoom', `Already in room: ${room}`); + return; + } + + if (this.pendingJoinSpan) { + this.pendingJoinSpan.setStatus({ code: 2, message: 'superseded' }); + this.pendingJoinSpan.end(); + this.pendingJoinSpan = null; + } + + startNewTrace(() => { + this.pendingJoinSpan = Sentry.startInactiveSpan({ + name: 'room.join', + op: 'session.join', + attributes: { 'room.name': sanitizedRoom }, + }); }); + this.wsService.send(`[UserCommand] /join ${sanitizedRoom}`); } /** @@ -113,6 +132,14 @@ export class RoomService implements IRoomService { this.ngZone.run(() => { this.currentRoom = matchJoin[2]; }); + + if (this.pendingJoinSpan) { + this.pendingJoinSpan.setAttribute('outcome', 'joined'); + this.pendingJoinSpan.setAttribute('room.confirmed', matchJoin[2]); + this.pendingJoinSpan.setStatus({ code: 1, message: 'ok' }); + this.pendingJoinSpan.end(); + this.pendingJoinSpan = null; + } } else { this.logger.warn('handleSystemMessage', `No room to join found in message: ${message}`); } From 6ed24bb06684e4eef2037ca2fd0a412521dc3a02 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:06:46 +0300 Subject: [PATCH 18/23] Web: Enhance WebRTC connection tracing - Start new tracing spans to improve observability. - Add status attributes for attempted and skipped connections. - Clean up spans and attempt counts on connection completion. --- .../communication/webrtc-signaling.service.ts | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts index 585c86cd..8e816879 100644 --- a/client/web/src/app/core/services/communication/webrtc-signaling.service.ts +++ b/client/web/src/app/core/services/communication/webrtc-signaling.service.ts @@ -1,5 +1,6 @@ import { Injectable, inject } from '@angular/core'; import * as Sentry from '@sentry/angular'; +import { startNewTrace } from '@sentry/core'; import { WebSocketConnectionService } from './websocket-connection.service'; import { UserService } from '../user-management/user.service'; import { @@ -77,15 +78,17 @@ export class WebRTCSignalingService { public initiateConnection(targetUser: string): void { let span = this.activeConnectSpans.get(targetUser); if (!span) { - span = Sentry.startInactiveSpan({ name: 'webrtc.connect', op: 'webrtc.connect' }); - this.activeConnectSpans.set(targetUser, span); + startNewTrace(() => { + span = Sentry.startInactiveSpan({ name: 'webrtc.connect', op: 'webrtc.connect' }); + this.activeConnectSpans.set(targetUser, span); + }); this.connectAttemptCounts.set(targetUser, 1); } else { const next = (this.connectAttemptCounts.get(targetUser) ?? 1) + 1; this.connectAttemptCounts.set(targetUser, next); span.setAttribute('attempts', next); } - this.initiateConnectionInner(targetUser, span); + this.initiateConnectionInner(targetUser, span!); } /** @@ -155,6 +158,13 @@ export class WebRTCSignalingService { if (this.connectionLocks.has(targetUser)) { this.logger.debug('initiateConnection', `Connection already in progress for ${targetUser}`); + if ((this.connectAttemptCounts.get(targetUser) ?? 0) === 1) { + span.setAttribute('outcome', 'skipped_lock'); + span.setStatus({ code: 1, message: 'already_in_progress' }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); + } return; } @@ -168,6 +178,11 @@ export class WebRTCSignalingService { 'initiateConnection', `PeerConnection with ${targetUser} is ${connectionState}` ); + span.setAttribute('outcome', `skipped_${connectionState}`); + span.setStatus({ code: 1, message: connectionState }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); return; } @@ -184,6 +199,11 @@ export class WebRTCSignalingService { 'initiateConnection', `PeerConnection with ${targetUser} exists in state ${connectionState}/${iceState}` ); + span.setAttribute('outcome', `skipped_${connectionState}_${iceState}`); + span.setStatus({ code: 1, message: 'unexpected_state' }); + span.end(); + this.activeConnectSpans.delete(targetUser); + this.connectAttemptCounts.delete(targetUser); return; } } @@ -233,6 +253,7 @@ export class WebRTCSignalingService { this.communicationService.sendQueuedMessages(targetUser); if (peerConnection.connectionState === 'connected') { this.reconnectAttempts.delete(targetUser); + this.finishConnectSpanAsSuccess(targetUser); this.peerConnected$.next(targetUser); } }; @@ -1172,6 +1193,7 @@ export class WebRTCSignalingService { this.communicationService.sendQueuedMessages(targetUser); if (peerConnection.connectionState === 'connected') { this.reconnectAttempts.delete(targetUser); + this.finishConnectSpanAsSuccess(targetUser); this.peerConnected$.next(targetUser); } }; From a51307e5435d872b97f27a0ffe63cedbaf1f7672 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:06:59 +0300 Subject: [PATCH 19/23] Web: Strengthen Sentry observability - Introduce Sentry TraceService with router dependency - Improve source map enabling condition for better debugging in browser contexts - Enhance error tracking and tracing capabilities --- client/web/src/app/app.config.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/web/src/app/app.config.ts b/client/web/src/app/app.config.ts index 2022bc1a..e3cf0723 100644 --- a/client/web/src/app/app.config.ts +++ b/client/web/src/app/app.config.ts @@ -7,6 +7,7 @@ import { provideZoneChangeDetection, } from '@angular/core'; import { provideRouter, withPreloading } from '@angular/router'; +import { Router } from '@angular/router'; import * as Sentry from '@sentry/angular'; import { routes } from './app.routes'; @@ -49,6 +50,8 @@ export const appConfig: ApplicationConfig = { provide: ErrorHandler, useValue: Sentry.createErrorHandler({ showDialog: false }), }, + { provide: Sentry.TraceService, deps: [Router] }, + provideAppInitializer(() => void inject(Sentry.TraceService)), provideHttpClient(withFetch()), provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }), provideRouter(routes, withPreloading(SelectivePreloadingStrategy)), @@ -80,7 +83,7 @@ export const appConfig: ApplicationConfig = { LoggerModule.forRoot({ level: environment.logLevel, timestampFormat: 'yyyy-MM-dd HH:mm:ss', - enableSourceMaps: environment.enableSourceMaps, + enableSourceMaps: typeof window !== 'undefined' && environment.enableSourceMaps, disableFileDetails: environment.disableFileDetails, disableConsoleLogging: environment.disableConsoleLogging, }) From 1f0d7a6070d2a85a8cd4d0ee4496aa7982ada998 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 19:12:35 +0300 Subject: [PATCH 20/23] Chore: Fix linting for .env.*.example. --- .env.development.example | 2 +- .env.production.example | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.development.example b/.env.development.example index 2567bdb6..d913370f 100644 --- a/.env.development.example +++ b/.env.development.example @@ -25,4 +25,4 @@ SERVER_NAME=127.0.0.1 # SENTRY_DSN must stay empty in this committed file — set it only in your # local .env.development (which is gitignored). SENTRY_DSN= -SENTRY_ENABLED=false \ No newline at end of file +SENTRY_ENABLED=false diff --git a/.env.production.example b/.env.production.example index 9b835e57..ab3222db 100644 --- a/.env.production.example +++ b/.env.production.example @@ -25,4 +25,4 @@ SERVER_NAME=pastepoint.com # Set SENTRY_DSN to the production server DSN. Without it, Sentry stays # disabled even if SENTRY_ENABLED=true. SENTRY_DSN= -SENTRY_ENABLED=true \ No newline at end of file +SENTRY_ENABLED=true From 464c565029f3823d42227574181afd22cbe9d6be Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 21:41:42 +0300 Subject: [PATCH 21/23] Make: Enforce .env file check for dev setup --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 3f7eaad6..a6bbe754 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ prod: # Development environment dev: @echo "Starting development environment..." + @test -f .env.development || (echo "Error: .env.development not found. Copy .env.development.example to .env.development and configure it." && exit 1) docker compose --env-file .env.development build --parallel docker compose --env-file .env.development up --force-recreate -d @echo "Development services are starting. View logs with: make logs" From 72802c49533fd9d816002149082ea32b77bc1890 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 21:42:14 +0300 Subject: [PATCH 22/23] Web: Removes unnecessary Sentry trace attributes for WebSocket and room management to streamline observability. --- .../core/services/communication/websocket-connection.service.ts | 2 +- .../web/src/app/core/services/room-management/room.service.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/client/web/src/app/core/services/communication/websocket-connection.service.ts b/client/web/src/app/core/services/communication/websocket-connection.service.ts index 32c22f87..7ca4c3b9 100644 --- a/client/web/src/app/core/services/communication/websocket-connection.service.ts +++ b/client/web/src/app/core/services/communication/websocket-connection.service.ts @@ -181,7 +181,7 @@ export class WebSocketConnectionService implements OnDestroy { connectSpan = Sentry.startInactiveSpan({ name: 'ws.connect', op: 'ws.connect', - attributes: { 'ws.url': wsUri, 'ws.session_code': code ?? 'public' }, + attributes: { 'ws.has_session_code': code != null }, }); }); diff --git a/client/web/src/app/core/services/room-management/room.service.ts b/client/web/src/app/core/services/room-management/room.service.ts index 0eb857fd..92f94fea 100644 --- a/client/web/src/app/core/services/room-management/room.service.ts +++ b/client/web/src/app/core/services/room-management/room.service.ts @@ -92,7 +92,6 @@ export class RoomService implements IRoomService { this.pendingJoinSpan = Sentry.startInactiveSpan({ name: 'room.join', op: 'session.join', - attributes: { 'room.name': sanitizedRoom }, }); }); this.wsService.send(`[UserCommand] /join ${sanitizedRoom}`); @@ -135,7 +134,6 @@ export class RoomService implements IRoomService { if (this.pendingJoinSpan) { this.pendingJoinSpan.setAttribute('outcome', 'joined'); - this.pendingJoinSpan.setAttribute('room.confirmed', matchJoin[2]); this.pendingJoinSpan.setStatus({ code: 1, message: 'ok' }); this.pendingJoinSpan.end(); this.pendingJoinSpan = null; From 891b47fc13e43ca425cea03dd4c40bc62f303101 Mon Sep 17 00:00:00 2001 From: Sulaiman AlRomaih Date: Fri, 8 May 2026 21:42:41 +0300 Subject: [PATCH 23/23] Server: Enhance optional Sentry transaction - Wrap Sentry transaction in `Option` to prevent crashes if the client is unavailable. --- server/src/handler.rs | 44 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/server/src/handler.rs b/server/src/handler.rs index d386df16..6595bf09 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -98,10 +98,12 @@ impl Handler for WsChatServer { type Result = (); fn handle(&mut self, msg: RelaySignalMessage, _ctx: &mut Self::Context) { - let tx = sentry::start_transaction(sentry::TransactionContext::new( - "signaling.relay", - "websocket.signal", - )); + let tx = sentry::Hub::current().client().map(|_| { + sentry::start_transaction(sentry::TransactionContext::new( + "signaling.relay", + "websocket.signal", + )) + }); let RelaySignalMessage { session_id, @@ -115,14 +117,18 @@ impl Handler for WsChatServer { target: "Websocket", "Skipping self-to-self signal from '{from}' to '{to}'" ); - tx.set_status(sentry::protocol::SpanStatus::Ok); - tx.finish(); + if let Some(tx) = tx { + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); + } return; } self.relay_message_to_user(&session_id, &to, message, &from); - tx.set_status(sentry::protocol::SpanStatus::Ok); - tx.finish(); + if let Some(tx) = tx { + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); + } } } @@ -141,10 +147,12 @@ impl Handler for WsChatServer { type Result = (); fn handle(&mut self, msg: ValidateAndRelaySignal, _ctx: &mut Self::Context) -> Self::Result { - let tx = sentry::start_transaction(sentry::TransactionContext::new( - "signaling.relay", - "websocket.signal", - )); + let tx = sentry::Hub::current().client().map(|_| { + sentry::start_transaction(sentry::TransactionContext::new( + "signaling.relay", + "websocket.signal", + )) + }); let shared_room = self.users_share_room(&msg.session_id, &msg.from_user, &msg.to_user); @@ -155,14 +163,18 @@ impl Handler for WsChatServer { msg.from_user, msg.to_user ); - tx.set_status(sentry::protocol::SpanStatus::PermissionDenied); - tx.finish(); + if let Some(tx) = tx { + tx.set_status(sentry::protocol::SpanStatus::PermissionDenied); + tx.finish(); + } return; } let relay_msg = ChatMessage(format!("{} {}", WS_PREFIX_SIGNAL_MESSAGE, msg.payload)); self.relay_message_to_user(&msg.session_id, &msg.to_user, relay_msg, &msg.from_user); - tx.set_status(sentry::protocol::SpanStatus::Ok); - tx.finish(); + if let Some(tx) = tx { + tx.set_status(sentry::protocol::SpanStatus::Ok); + tx.finish(); + } } }