Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ make electron
- **Thread-Safe** - Handles concurrent requests with isolated mappings
- **Desktop UI** - Native Electron app for macOS with visual request monitoring
- **Production Ready** - Systemd service, Docker support, comprehensive logging
- **Privacy First** - All processing happens locally, no external dependencies
- **Privacy First** - All PII processing happens locally; crash/error telemetry is [opt-in and off by default](docs/05-advanced-topics.md#telemetry--privacy), and never includes your prompts or unmasked text

---

Expand Down
4 changes: 4 additions & 0 deletions docs/02-development-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,10 @@ export MODEL_PATH="model/quantized"
export LOG_REQUESTS="true"
export LOG_PII_CHANGES="true"

# Telemetry (crash & error reporting via Sentry) — opt-in, off by default.
# See docs/05-advanced-topics.md → Telemetry & Privacy.
export KIJI_TELEMETRY_ENABLED="false"

# Database (usually disabled in dev)
export DB_ENABLED="false"

Expand Down
14 changes: 14 additions & 0 deletions docs/03-building-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,20 @@ KIJI_BASIC_AUTH_PASSWORD=change-me
- Set `KIJI_BASIC_AUTH_ENABLED=false` to disable auth even when credentials are
present (only the literal `true` keeps it on).

### Telemetry (Crash & Error Reporting)

Telemetry is **opt-in and off by default**. To send crash/error reports to Sentry
from a server deployment, set:

```bash
# add to /etc/kiji-proxy.env
KIJI_TELEMETRY_ENABLED=true # only the literal "true" enables it
```

Prompts, request/response bodies, and unmasked input are never sent. See
[Telemetry & Privacy](05-advanced-topics.md#telemetry--privacy) for the full list
of what is and isn't collected.

### Docker Deployment

```dockerfile
Expand Down
49 changes: 49 additions & 0 deletions docs/05-advanced-topics.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This chapter covers advanced features, security considerations, and troubleshoot
- [Model Signing](#model-signing)
- [Build Troubleshooting](#build-troubleshooting)
- [Performance Optimization](#performance-optimization)
- [Telemetry & Privacy](#telemetry--privacy)
- [Security Best Practices](#security-best-practices)

## Transparent Proxy & MITM
Expand Down Expand Up @@ -1099,6 +1100,54 @@ ls -la build/tokenizers/libtokenizers.a && echo "Cached" || echo "Will rebuild"
- Enable HTTP/2 when available
- Configure appropriate timeouts

## Telemetry & Privacy

Kiji uses [Sentry](https://sentry.io) for crash and error reporting. Because Kiji
is a privacy proxy, telemetry is **opt-in and disabled by default** — nothing is
sent unless you explicitly turn it on.

### What is (and isn't) sent

When telemetry is enabled, Kiji may send:

- ✅ Uncaught exceptions and crashes (stack traces, error messages)
- ✅ Backend panics (via the HTTP handler middleware and the main goroutine)
- ✅ PII misclassification reports you explicitly submit — **masked** text plus
entity metadata only (entity type, replacement token, confidence)
- ✅ Basic environment info (app version, `production`/`development`)

Kiji never sends:

- ❌ Your prompts or request/response bodies
- ❌ The original, **unmasked** input text
- ❌ The raw matched PII substrings (only the entity *type* and *confidence*)
- ❌ Your API keys or provider credentials

Performance tracing is sampled at 10% (`tracesSampleRate: 0.1`).

### Enabling / disabling

**Desktop app (macOS):** Settings → Advanced → **Crash & Error Reporting**. The
choice is persisted in the Electron config (`telemetryEnabled`). Restart the app
after changing it so all three processes (renderer, Electron main, and the Go
backend) pick up the new value.

**Server / headless backend (Linux):** telemetry is controlled by an environment
variable and defaults to off:

```bash
# Opt in to error/crash reporting
export KIJI_TELEMETRY_ENABLED=true

# Leave unset (or set to anything other than "true") to keep it off
```

The desktop app forwards your Settings choice to the Go backend through this same
`KIJI_TELEMETRY_ENABLED` variable when it spawns the backend process.

> Submitting a PII misclassification report requires telemetry to be enabled — if
> it is off, the report is not sent and the app tells you so.

## Security Best Practices

### Development
Expand Down
26 changes: 11 additions & 15 deletions src/backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/backend/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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,
Expand Down
101 changes: 101 additions & 0 deletions src/backend/telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -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)
}
27 changes: 21 additions & 6 deletions src/frontend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions src/frontend/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -37,8 +38,9 @@ class ErrorBoundary extends Component<Props, State> {
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 => {
Expand Down
12 changes: 12 additions & 0 deletions src/frontend/src/components/modals/MisclassificationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ export default function MisclassificationModal({
</div>
</div>

<div className="p-6 pt-0">
<p className="text-xs text-stone-500 flex items-start gap-1.5">
<AlertCircle className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
<span>
Only the <strong>masked</strong> text and entity metadata (type,
confidence) are sent — never the original input shown above.
Reports are sent only if crash &amp; error reporting is enabled
in Settings.
</span>
</p>
</div>

<div className="flex gap-3 p-6 pt-0 border-t border-stone-100">
<button
type="submit"
Expand Down
Loading
Loading