From bd3005c20ca52fdb8d733f30d041985145490d9f Mon Sep 17 00:00:00 2001 From: Hannes Hapke Date: Sat, 11 Jul 2026 12:11:07 -0700 Subject: [PATCH 1/2] made sentry optional --- src/backend/main.go | 26 ++--- src/backend/server/server.go | 6 ++ src/backend/telemetry/telemetry.go | 101 ++++++++++++++++++ src/frontend/index.js | 27 +++-- src/frontend/src/components/ErrorBoundary.tsx | 6 +- .../modals/MisclassificationModal.tsx | 12 +++ .../components/settings/AdvancedSection.tsx | 83 +++++++++++++- src/frontend/src/electron/electron-main.js | 50 +++++++-- src/frontend/src/electron/electron-preload.js | 9 ++ src/frontend/src/electron/electron.d.ts | 6 ++ src/frontend/src/electron/ipc-handlers.js | 12 +++ .../src/hooks/useMisclassificationReport.ts | 18 +++- src/frontend/src/telemetry/sentry.js | 23 ++++ .../src/utils/misclassificationReporter.ts | 58 ++++++---- 14 files changed, 382 insertions(+), 55 deletions(-) create mode 100644 src/backend/telemetry/telemetry.go create mode 100644 src/frontend/src/telemetry/sentry.js diff --git a/src/backend/main.go b/src/backend/main.go index ebcbd99e..786ecf96 100644 --- a/src/backend/main.go +++ b/src/backend/main.go @@ -13,9 +13,9 @@ import ( "strings" "time" - "github.com/getsentry/sentry-go" "github.com/dataiku/kiji-proxy/src/backend/config" "github.com/dataiku/kiji-proxy/src/backend/server" + "github.com/dataiku/kiji-proxy/src/backend/telemetry" "github.com/joho/godotenv" ) @@ -49,33 +49,29 @@ func main() { log.Printf("Note: .env file not found or could not be loaded: %v", err) } - // Initialize Sentry for error tracking (deferred flush happens in run) + // Initialize telemetry (error/crash reporting). Opt-in: nothing is sent + // unless KIJI_TELEMETRY_ENABLED=true. The desktop app forwards the user's + // Settings choice through this env var; server deployments set it directly. environment := "production" if os.Getenv("NODE_ENV") == "development" { environment = "development" } - err := sentry.Init(sentry.ClientOptions{ - Dsn: "https://d7ad4213601549253c0d313b271f83cf@o4510660510679040.ingest.de.sentry.io/4510660556095568", - Environment: environment, - Release: version, - TracesSampleRate: 1.0, - }) - if err != nil { - log.Printf("Warning: Failed to initialize Sentry: %v", err) - } else { - log.Println("Sentry initialized successfully") - } + telemetry.Init(os.Getenv("KIJI_TELEMETRY_ENABLED") == TRUE, environment, version) // Run main logic if err := run(configPath); err != nil { log.Printf("Fatal error: %v", err) - sentry.CaptureException(err) - sentry.Flush(2 * time.Second) + telemetry.CaptureException(err) + telemetry.Flush(2 * time.Second) os.Exit(1) } } func run(configPath *string) error { + // Report an unhandled panic in the main goroutine to telemetry (if enabled) + // before it crashes the process. Re-panics to preserve the original behavior. + defer telemetry.Recover() + // Log version at startup with banner log.Println("================================================================================") log.Printf("🚀 Starting Dataiku's Kiji Privacy Proxy v%s", version) diff --git a/src/backend/server/server.go b/src/backend/server/server.go index 6a82968d..2ee30457 100644 --- a/src/backend/server/server.go +++ b/src/backend/server/server.go @@ -21,6 +21,7 @@ import ( "github.com/dataiku/kiji-proxy/src/backend/paths" "github.com/dataiku/kiji-proxy/src/backend/providers" "github.com/dataiku/kiji-proxy/src/backend/proxy" + "github.com/dataiku/kiji-proxy/src/backend/telemetry" "golang.org/x/time/rate" ) @@ -342,6 +343,11 @@ func (s *Server) Start() error { rootHandler = s.basicAuthMiddleware(mux) } + // Report panics in any HTTP handler to telemetry (when enabled) before the + // request is aborted. Repanic is on inside the middleware, so net/http's own + // per-connection recovery still runs and the server stays up as before. + rootHandler = telemetry.HTTPMiddleware(rootHandler) + // Create server with timeout configuration server := &http.Server{ Addr: s.config.ProxyPort, diff --git a/src/backend/telemetry/telemetry.go b/src/backend/telemetry/telemetry.go new file mode 100644 index 00000000..1d6f2bb4 --- /dev/null +++ b/src/backend/telemetry/telemetry.go @@ -0,0 +1,101 @@ +// Package telemetry centralizes all Sentry (error-reporting) wiring for the Go +// backend. It is the single source of truth for the Sentry DSN and options, and +// every other package must go through it rather than importing sentry-go +// directly. +// +// Telemetry is OPT-IN. Nothing is sent unless the operator explicitly enables it +// by setting KIJI_TELEMETRY_ENABLED=true (the desktop app forwards the user's +// Settings choice through this env var when it spawns the backend). When it is +// disabled, Init is a no-op and every other function here short-circuits, so no +// events, traces, or panics ever leave the machine — important for a privacy +// proxy. +package telemetry + +import ( + "log" + "net/http" + "time" + + "github.com/getsentry/sentry-go" + sentryhttp "github.com/getsentry/sentry-go/http" +) + +// dsn is the Sentry ingest endpoint. It is a public (client-side) DSN, so it is +// safe to embed; it only permits sending events, not reading them. Defined once +// here so the backend never duplicates it (the frontend has its own single copy +// in src/frontend/src/telemetry/sentry.js). +const dsn = "https://d7ad4213601549253c0d313b271f83cf@o4510660510679040.ingest.de.sentry.io/4510660556095568" + +// tracesSampleRate keeps performance tracing light: 10% of transactions rather +// than the previous 100%, which was needlessly expensive. +const tracesSampleRate = 0.1 + +// enabled records whether Init actually brought Sentry up. All exported helpers +// short-circuit on it so callers can invoke them unconditionally. +var enabled bool + +// Init brings up Sentry when enable is true; otherwise it does nothing (and +// leaves every other helper a no-op). environment is "production"/"development"; +// release is the build version string. +func Init(enable bool, environment, release string) { + if !enable { + log.Println("Telemetry disabled (opt-in). Set KIJI_TELEMETRY_ENABLED=true to send crash/error reports to Sentry.") + return + } + + if err := sentry.Init(sentry.ClientOptions{ + Dsn: dsn, + Environment: environment, + Release: release, + TracesSampleRate: tracesSampleRate, + }); err != nil { + log.Printf("Warning: Failed to initialize Sentry: %v", err) + return + } + + enabled = true + log.Println("Telemetry enabled: Sentry initialized (error and crash reporting active)") +} + +// Enabled reports whether telemetry is active. +func Enabled() bool { return enabled } + +// CaptureException reports err to Sentry when telemetry is enabled. +func CaptureException(err error) { + if !enabled { + return + } + sentry.CaptureException(err) +} + +// Flush blocks until buffered events are sent or timeout elapses. Safe to call +// when telemetry is disabled (no-op). +func Flush(timeout time.Duration) { + if !enabled { + return + } + sentry.Flush(timeout) +} + +// Recover captures a panic in the current goroutine and forwards it to Sentry, +// then re-panics so the process keeps its original crash behavior. Use as +// `defer telemetry.Recover()` at the top of a goroutine's entry function. It is +// a no-op (and does not swallow the panic) when telemetry is disabled. +func Recover() { + if r := recover(); r != nil { + if enabled { + sentry.CurrentHub().Recover(r) + sentry.Flush(2 * time.Second) + } + panic(r) + } +} + +// HTTPMiddleware wraps an http.Handler so panics in request handlers are +// reported to Sentry before the request is aborted. Repanic is on, so net/http's +// per-connection recovery still runs and the server stays up exactly as before. +// When telemetry is disabled the wrapper is still installed but reports nothing +// (the current hub has no client), so behavior is unchanged. +func HTTPMiddleware(next http.Handler) http.Handler { + return sentryhttp.New(sentryhttp.Options{Repanic: true}).Handle(next) +} diff --git a/src/frontend/index.js b/src/frontend/index.js index 93d5a280..b7b413bd 100644 --- a/src/frontend/index.js +++ b/src/frontend/index.js @@ -4,13 +4,28 @@ import "./src/styles/styles.css"; import AppShell from "./src/components/AppShell.tsx"; import ErrorBoundary from "./src/components/ErrorBoundary.tsx"; import * as Sentry from "@sentry/electron/renderer"; +import { SENTRY_DSN, SENTRY_TRACES_SAMPLE_RATE } from "./src/telemetry/sentry"; -// Initialize Sentry for renderer process -Sentry.init({ - dsn: "https://d7ad4213601549253c0d313b271f83cf@o4510660510679040.ingest.de.sentry.io/4510660556095568", - environment: process.env.NODE_ENV || "production", - tracesSampleRate: 1.0, -}); +// Telemetry (Sentry) is OPT-IN: only initialize the renderer SDK when the user +// enabled "Crash & error reporting" in Settings (persisted in the Electron +// config, read here over IPC). When disabled, Sentry is never initialized, so +// every capture call elsewhere in the renderer is a harmless no-op. +async function initTelemetryRenderer() { + try { + const enabled = await window.electronAPI?.getTelemetryEnabled?.(); + if (!enabled) return; + Sentry.init({ + dsn: SENTRY_DSN, + environment: process.env.NODE_ENV || "production", + tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE, + }); + } catch (error) { + console.error("[Telemetry] Failed to initialize renderer Sentry:", error); + } +} + +// Fire-and-forget: rendering must not wait on the telemetry preference lookup. +initTelemetryRenderer(); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( diff --git a/src/frontend/src/components/ErrorBoundary.tsx b/src/frontend/src/components/ErrorBoundary.tsx index ddeb8053..fcea20f6 100644 --- a/src/frontend/src/components/ErrorBoundary.tsx +++ b/src/frontend/src/components/ErrorBoundary.tsx @@ -1,5 +1,6 @@ import { Component, ErrorInfo, ReactNode } from "react"; import { AlertCircle, RefreshCw } from "lucide-react"; +import { reportError } from "../utils/misclassificationReporter"; interface Props { children: ReactNode; @@ -37,8 +38,9 @@ class ErrorBoundary extends Component { errorInfo, }); - // In a production app, you might want to log this to an error reporting service - // e.g., Sentry, LogRocket, etc. + // Forward to Sentry. This is a no-op when telemetry is disabled (opt-in), + // so no data leaves the machine unless the user turned reporting on. + reportError(error, { componentStack: errorInfo.componentStack }); } handleReset = (): void => { diff --git a/src/frontend/src/components/modals/MisclassificationModal.tsx b/src/frontend/src/components/modals/MisclassificationModal.tsx index d0f36602..1026048b 100644 --- a/src/frontend/src/components/modals/MisclassificationModal.tsx +++ b/src/frontend/src/components/modals/MisclassificationModal.tsx @@ -123,6 +123,18 @@ export default function MisclassificationModal({ +
+

+ + + Only the masked text and entity metadata (type, + confidence) are sent — never the original input shown above. + Reports are sent only if crash & error reporting is enabled + in Settings. + +

+
+
+ {/* Crash & Error Reporting (Telemetry) Opt-in */} +
+
+ + +
+

+ Off by default. When enabled, anonymous crash and error reports are + sent to help improve Kiji. Your prompts and the original (unmasked) + text are never sent — only masked text and technical error details. + Enabling this also lets you submit PII misclassification reports. +

+ {telemetryNeedsRestart && ( +
+

+ Restart the app for this change to fully take effect across all + components. +

+
+ )} +
+ {/* Load Custom Kiji PII Model */}