diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ab311423..da70b407 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -93,6 +93,8 @@ jobs: NIXMAC_ENV: prod NIXMAC_VERSION: ${{ steps.sync-version.outputs.build_version }} VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }} + VITE_SERVER_URL: ${{ secrets.VITE_SERVER_URL }} + SUBMITTED_FEEDBACK_DSN: ${{ secrets.SUBMITTED_FEEDBACK_DSN }} VITE_NIXMAC_ENV: prod VITE_NIXMAC_VERSION: ${{ steps.sync-version.outputs.build_version }} - name: Unit Test Tauri app diff --git a/apps/native/src-tauri/build.rs b/apps/native/src-tauri/build.rs index fbc34524..a100f811 100644 --- a/apps/native/src-tauri/build.rs +++ b/apps/native/src-tauri/build.rs @@ -1,16 +1,20 @@ fn main() { - // If `SENTRY_DSN` is provided in the build environment (set by CI), - // expose it as a compile-time environment variable so it is embedded - // into the binary via `env!` or `option_env!`. - if let Ok(dsn) = std::env::var("SENTRY_DSN") { - println!("cargo:rustc-env=SENTRY_DSN={}", dsn); - } + // Set up passthrough for relevant environment variables. + // This allows configuration to be injected at build time (e.g. by CI) + // or in development environments. + for key in [ + "SENTRY_DSN", + "VITE_SERVER_URL", + "SUBMITTED_FEEDBACK_DSN", + "NIXMAC_ENV", + ] { + println!("cargo:rerun-if-env-changed={key}"); - // If `NIXMAC_ENV` is provided in the build environment, embed it so the - // binary can read it at compile time via `option_env!`. - if let Ok(env) = std::env::var("NIXMAC_ENV") { - println!("cargo:rustc-env=NIXMAC_ENV={}", env); + if let Ok(value) = std::env::var(key) { + println!("cargo:rustc-env={key}={value}"); + } } + // Determine the version to embed. Prefer an explicit `NIXMAC_VERSION` // from the build environment (e.g. set by CI). If not present, fall // back to the Cargo package version, and finally to "unknown". diff --git a/apps/native/src-tauri/src/feedback.rs b/apps/native/src-tauri/src/feedback.rs index 21f2ef77..b9ff1749 100644 --- a/apps/native/src-tauri/src/feedback.rs +++ b/apps/native/src-tauri/src/feedback.rs @@ -496,7 +496,13 @@ fn get_report_path(app: &AppHandle) -> Result { /// Submit feedback: try to POST, save to disk on failure, also flush any pending reports. /// Returns true if the new submission was sent successfully. pub async fn submit(app: &AppHandle, payload: String) -> Result { - let feedback_url = get_feedback_url(); + let feedback_url = match get_feedback_url() { + Ok(url) => url, + Err(_) => { + log::error!("[feedback] Feedback system is not configured"); + return Ok(false); + } + }; let parsed: Value = serde_json::from_str(&payload).unwrap_or(Value::String(payload.clone())); let client = reqwest::Client::new(); @@ -549,19 +555,32 @@ fn save_to_queue(app: &AppHandle, payload: &Value, failure_reason: &str) -> Resu Ok(()) } -const FEEDBACK_DSN: &str = "dsn_6f4b9a5e8c2d4f1a9b3c7e2d5a1f0b4c"; - -fn get_feedback_url() -> String { +/// Construct the full feedback submission URL from environment configuration. +/// If VITE_SERVER_URL is not set, returns an error indicating feedback is not configured. +/// In a production binary, VITE_SERVER_URL should be embedded at build time; +/// in development it can be set in the environment. +fn get_feedback_url() -> Result { let base = option_env!("VITE_SERVER_URL") .map(|s| s.to_string()) .or_else(|| std::env::var("VITE_SERVER_URL").ok()) - .unwrap_or_else(|| "http://localhost:3001".to_string()); - format!("{}/api/feedback/{}", base, FEEDBACK_DSN) + .ok_or_else(|| anyhow::anyhow!("sending feedback not configured (url)"))?; + + let dsn = option_env!("SUBMITTED_FEEDBACK_DSN") + .map(|s| s.to_string()) + .or_else(|| std::env::var("SUBMITTED_FEEDBACK_DSN").ok()) + .ok_or_else(|| anyhow::anyhow!("sending feedback not configured (dsn)"))?; + + Ok(format!("{}/api/feedback/{}", base, dsn)) } /// Retry all pending feedback reports in the background. /// Reads report.json, POSTs each entry, removes successes. pub async fn retry_pending(app: &AppHandle) -> Result { + let feedback_url = match get_feedback_url() { + Ok(url) => url, + Err(_) => return Ok(0), // If feedback is not configured, skip retrying + }; + let path = get_report_path(app)?; if !path.exists() { @@ -577,7 +596,6 @@ pub async fn retry_pending(app: &AppHandle) -> Result { return Ok(0); } - let feedback_url = get_feedback_url(); log::info!( "[feedback] Found {} pending report(s), sending to {}", entries.len(), @@ -858,4 +876,26 @@ regex = "token=([A-Za-z0-9]+)" "before [REDACTED (High Entropy)] after" ); } + + #[test] + fn test_get_feedback_url() { + // Backup original env var + let original = std::env::var("VITE_SERVER_URL").ok(); + + // Test with env var set + std::env::set_var("VITE_SERVER_URL", "https://example.com"); + std::env::set_var("SUBMITTED_FEEDBACK_DSN", "test-dsn"); + let url = super::get_feedback_url().unwrap(); + assert_eq!(url, "https://example.com/api/feedback/test-dsn"); + + // Test with env var missing + std::env::remove_var("VITE_SERVER_URL"); + std::env::remove_var("SUBMITTED_FEEDBACK_DSN"); + assert!(super::get_feedback_url().is_err()); + + // Restore original env var + if let Some(val) = original { + std::env::set_var("VITE_SERVER_URL", val); + } + } }