This diff introduces significant improvements in both frontend error handling (telemetry) and backend observability (metrics/logs export).
Here is a detailed summary of the changes: ### 🚀 Frontend Telemetry Improvement (`src/lib/telemetry.ts` & `src/main.ts`) A dedicated module for robust, rate-limited error reporting has been added to the frontend application. * **Error Handling:** The new `installErrorReporting` function registers global listeners for `error` and `unhandledrejection` events on the window object. * **Stability Focus:** To prevent telemetry from interfering with the main user experience, the system implements a **rate limit** (`MAX_REPORTS_PER_SESSION = 25`) and checks to ensure duplicate messages are not sent repeatedly during a single session. * **Integration:** `src/main.ts` now calls `installErrorReporting()` upon application startup, ensuring that error monitoring is active from the moment the app loads. ### ⚙️ Backend Metrics & Logs Export Enhancement (Rust Code Block) The Rust code block significantly refactors and enhances the logic for exporting system metrics and application logs to an OpenTelemetry Collector endpoint. The structure now adheres much more closely to standard OTLP/JSON schemas, and unit tests are added for validation. #### 1. Metrics Export (`gauge` function & main loop) * **Comprehensive Data Collection:** The export loop is updated to refresh process information and calculate key metrics: * Process Memory Usage (`process.memory.usage`) * CPU Utilization (System-wide, normalized) (`process.cpu.utilization`) * System Memory Utilization (`system.memory.utilization`) * **Standardized Payload:** The `gauge` helper function correctly structures these metrics into the required OpenTelemetry JSON format, including resource attributes and time points. #### 2. Logs Export (`logs_payload`, `resource`, etc.) * **Structured Logging:** The `logs_payload` function provides a robust way to serialize application logs (`LogEvent`). It accurately maps log severity, body content, and custom attributes into the OTLP/JSON format. * **Resource Attributes:** Helper functions like `resource()` ensure that all telemetry payloads are correctly tagged with essential metadata (service name, version, OS type, architecture). #### 3. Testing * **Validation Added:** Comprehensive unit tests (`#[cfg(test)] mod tests`) have been added to validate the complex serialization logic for both metrics and logs, ensuring they match the expected OpenTelemetry schema structure. *** ### Summary of Impact These changes result in a much more observable and stable application: 1. **Improved Observability:** The system can now reliably capture detailed performance data (CPU/Memory) and structured event logs from the backend, while simultaneously capturing critical runtime errors from the frontend. 2. **Increased Stability:** Rate limiting on error reporting prevents telemetry failures from cascading into user-facing bugs. 3. **Code Quality:** The addition of unit tests for the complex serialization logic significantly increases confidence in the reliability of the data export pipeline.
This commit is contained in:
@@ -118,7 +118,11 @@
|
||||
"Bash(node -e \"const fs=require\\('fs'\\); const version='2026.8.1'; const pkgbuild=fs.readFileSync\\('PKGBUILD','utf8'\\).replace\\(/^pkgver=.*\\\\$/m, 'pkgver='+version\\).replace\\(/^pkgrel=.*\\\\$/m, 'pkgrel=1'\\); fs.writeFileSync\\('PKGBUILD', pkgbuild\\);\")",
|
||||
"Bash(rm PKGBUILD)",
|
||||
"Bash(git -C /mnt/data/Development/GitLite stash)",
|
||||
"Bash(git -C /mnt/data/Development/GitLite stash pop)"
|
||||
"Bash(git -C /mnt/data/Development/GitLite stash pop)",
|
||||
"Bash(cargo clean *)",
|
||||
"Bash(cargo test *)",
|
||||
"Bash(sed -i \"s/TIMESTAMP/$\\(date +%s%N\\)/\" otlp-test.json)",
|
||||
"Bash(curl -sS -w '\\\\nHTTP %{http_code}\\\\n' -X POST https://telemetry.cbsk-tech.de/v1/logs -H 'Content-Type: application/json' --data @otlp-test.json --max-time 15)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# SigNoz-Telemetrie
|
||||
|
||||
Gitty meldet Abstürze, Fehler und Ressourcenverbrauch an eine SigNoz-Instanz
|
||||
(OTLP/HTTP, JSON). Implementierung: `src-tauri/src/telemetry.rs` plus
|
||||
`src/lib/telemetry.ts` für Frontend-Fehler.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
Der Collector-Endpoint ist als Konstante `OTLP_ENDPOINT` in
|
||||
`src-tauri/src/telemetry.rs` hinterlegt (`http://telemetry.cbsk-tech.de`,
|
||||
analog zum fest eingetragenen Aptabase-Host in `main.rs`). Telemetrie ist
|
||||
damit in jedem Build aktiv — auch im Dev-Modus.
|
||||
|
||||
## Was gemeldet wird
|
||||
|
||||
- **Abstürze**: Ein Panic-Hook schickt Panics als `FATAL`-Log mit Backtrace und
|
||||
Quellposition — synchron, bevor der Prozess stirbt (SigNoz: *Logs*, Filter
|
||||
`error.kind=panic`).
|
||||
- **Rust-Fehler/-Warnungen**: Alles, was über das `log`-Crate mit `warn!`/
|
||||
`error!` geloggt wird, geht zusätzlich zur Konsole als Log-Record raus.
|
||||
- **Frontend-Fehler**: Uncaught Exceptions und unbehandelte Promise-Rejections
|
||||
aus dem WebView (Attribut `component=frontend`), pro Sitzung dedupliziert
|
||||
und auf 25 Meldungen gedeckelt.
|
||||
- **Ressourcen** (alle 60 s als Gauges): `process.memory.usage` (Bytes),
|
||||
`process.cpu.utilization` (0–1, über alle Kerne normalisiert),
|
||||
`system.memory.utilization` (0–1).
|
||||
|
||||
Alle Daten tragen die Resource-Attribute `service.name=gitty`,
|
||||
`service.version` (App-Version aus `tauri.conf.json`), `os.type` und
|
||||
`host.arch`. Es werden keine Repository-Inhalte, Pfade oder Nutzerdaten
|
||||
übertragen — nur Fehlermeldungstexte, Stacktraces und Prozessmetriken.
|
||||
Fehlgeschlagene Exporte werden verworfen; Telemetrie darf den Git-Workflow
|
||||
nie stören.
|
||||
Generated
+2
@@ -2538,8 +2538,10 @@ dependencies = [
|
||||
"commit_ai",
|
||||
"keyring",
|
||||
"log",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-aptabase",
|
||||
|
||||
@@ -22,6 +22,10 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "async-
|
||||
commit_ai = { path = "crates/commit_ai" }
|
||||
tokio = "1.52.3"
|
||||
log = "0.4"
|
||||
# SigNoz telemetry (see src/telemetry.rs). Both crates are already in the
|
||||
# dependency tree via commit_ai/mistralrs, so this costs little binary size.
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "blocking"] }
|
||||
sysinfo = { version = "0.36", default-features = false, features = ["system"] }
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
+14
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
mod badge;
|
||||
mod git;
|
||||
mod telemetry;
|
||||
|
||||
use badge::set_sync_badge;
|
||||
use git::{
|
||||
@@ -40,6 +41,7 @@ impl log::Log for ConsoleLogger {
|
||||
record.target(),
|
||||
record.args()
|
||||
);
|
||||
telemetry::forward_log_record(record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +88,15 @@ async fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
let context = tauri::generate_context!();
|
||||
telemetry::init(
|
||||
context
|
||||
.config()
|
||||
.version
|
||||
.clone()
|
||||
.unwrap_or_else(|| "unknown".to_string()),
|
||||
);
|
||||
|
||||
let builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||
#[cfg(desktop)]
|
||||
@@ -193,8 +204,9 @@ async fn main() {
|
||||
cred_save,
|
||||
cred_delete,
|
||||
set_sync_badge,
|
||||
close_splashscreen
|
||||
close_splashscreen,
|
||||
telemetry::report_frontend_error
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
//! Crash, error, and resource reporting to the self-hosted SigNoz instance
|
||||
//! via OTLP/HTTP (JSON).
|
||||
//!
|
||||
//! A hand-rolled OTLP JSON payload keeps the binary small: the official
|
||||
//! `opentelemetry` crates would pull in tonic/prost for functionality this app
|
||||
//! doesn't need beyond logs and a handful of gauges.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Self-hosted SigNoz OTLP/HTTP collector (same infrastructure as the
|
||||
/// Aptabase host configured in main.rs).
|
||||
const OTLP_ENDPOINT: &str = "https://telemetry.cbsk-tech.de";
|
||||
|
||||
const LOG_BATCH_SIZE: usize = 20;
|
||||
const LOG_FLUSH_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const METRICS_INTERVAL: Duration = Duration::from_secs(60);
|
||||
/// Bodies and stack traces are truncated so a pathological error message can't
|
||||
/// produce megabyte-sized export payloads.
|
||||
const MAX_TEXT_LEN: usize = 8_000;
|
||||
|
||||
pub const SEVERITY_WARN: (u8, &str) = (13, "WARN");
|
||||
pub const SEVERITY_ERROR: (u8, &str) = (17, "ERROR");
|
||||
const SEVERITY_FATAL: (u8, &str) = (21, "FATAL");
|
||||
|
||||
pub struct LogEvent {
|
||||
time_unix_nano: u128,
|
||||
severity: (u8, &'static str),
|
||||
body: String,
|
||||
attributes: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl LogEvent {
|
||||
pub fn new(severity: (u8, &'static str), body: impl Into<String>) -> Self {
|
||||
Self {
|
||||
time_unix_nano: now_unix_nanos(),
|
||||
severity,
|
||||
body: truncate(body.into()),
|
||||
attributes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attribute(mut self, key: &str, value: impl Into<String>) -> Self {
|
||||
self.attributes.push((key.to_string(), truncate(value.into())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct Config {
|
||||
logs_url: String,
|
||||
metrics_url: String,
|
||||
app_version: String,
|
||||
}
|
||||
|
||||
static CONFIG: OnceLock<Config> = OnceLock::new();
|
||||
static SENDER: OnceLock<mpsc::UnboundedSender<LogEvent>> = OnceLock::new();
|
||||
|
||||
fn now_unix_nanos() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn truncate(mut text: String) -> String {
|
||||
if text.len() > MAX_TEXT_LEN {
|
||||
let mut cut = MAX_TEXT_LEN;
|
||||
while !text.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
text.truncate(cut);
|
||||
text.push_str(" …[truncated]");
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
/// Initializes telemetry export. Must run inside the tokio runtime.
|
||||
pub fn init(app_version: String) {
|
||||
if CONFIG
|
||||
.set(Config {
|
||||
logs_url: format!("{OTLP_ENDPOINT}/v1/logs"),
|
||||
metrics_url: format!("{OTLP_ENDPOINT}/v1/metrics"),
|
||||
app_version,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
let _ = SENDER.set(tx);
|
||||
|
||||
install_panic_hook();
|
||||
tokio::spawn(run_log_exporter(rx));
|
||||
tokio::spawn(run_metrics_exporter());
|
||||
}
|
||||
|
||||
/// Queues a log record for export. Safe to call from any thread; a no-op
|
||||
/// before `init`.
|
||||
pub fn emit(event: LogEvent) {
|
||||
if let Some(tx) = SENDER.get() {
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
}
|
||||
|
||||
/// Forwards a `log` crate record. Filters out the HTTP stack used by the
|
||||
/// exporter itself so a failing export can never loop back into the queue.
|
||||
pub fn forward_log_record(record: &log::Record<'_>) {
|
||||
let target = record.target();
|
||||
if ["reqwest", "hyper", "rustls", "h2"]
|
||||
.iter()
|
||||
.any(|noisy| target.starts_with(noisy))
|
||||
{
|
||||
return;
|
||||
}
|
||||
let severity = match record.level() {
|
||||
log::Level::Error => SEVERITY_ERROR,
|
||||
log::Level::Warn => SEVERITY_WARN,
|
||||
_ => return,
|
||||
};
|
||||
emit(LogEvent::new(severity, record.args().to_string()).attribute("log.target", target));
|
||||
}
|
||||
|
||||
/// Receives uncaught frontend errors (window `error` / `unhandledrejection`).
|
||||
#[tauri::command]
|
||||
pub fn report_frontend_error(message: String, stack: Option<String>, source: Option<String>) {
|
||||
let mut event =
|
||||
LogEvent::new(SEVERITY_ERROR, message).attribute("component", "frontend");
|
||||
if let Some(stack) = stack {
|
||||
event = event.attribute("exception.stacktrace", stack);
|
||||
}
|
||||
if let Some(source) = source {
|
||||
event = event.attribute("code.filepath", source);
|
||||
}
|
||||
emit(event);
|
||||
}
|
||||
|
||||
fn install_panic_hook() {
|
||||
let previous = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
previous(info);
|
||||
|
||||
let message = info
|
||||
.payload()
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| info.payload().downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "panic with non-string payload".to_string());
|
||||
let location = info
|
||||
.location()
|
||||
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let backtrace = std::backtrace::Backtrace::force_capture().to_string();
|
||||
|
||||
let event = LogEvent::new(SEVERITY_FATAL, format!("panic at {location}: {message}"))
|
||||
.attribute("error.kind", "panic")
|
||||
.attribute("exception.stacktrace", backtrace)
|
||||
.attribute("code.filepath", location);
|
||||
|
||||
// The async exporter may never get to run again, so ship the crash on
|
||||
// a dedicated thread with a blocking client and wait for it.
|
||||
let payload = logs_payload(std::iter::once(&event));
|
||||
let _ = std::thread::spawn(move || post_blocking(&payload)).join();
|
||||
}));
|
||||
}
|
||||
|
||||
fn post_blocking(payload: &Value) {
|
||||
let Some(cfg) = CONFIG.get() else { return };
|
||||
let Ok(client) = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let _ = client.post(&cfg.logs_url).json(payload).send();
|
||||
}
|
||||
|
||||
async fn run_log_exporter(mut rx: mpsc::UnboundedReceiver<LogEvent>) {
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return,
|
||||
};
|
||||
let mut batch: Vec<LogEvent> = Vec::new();
|
||||
let mut ticker = tokio::time::interval(LOG_FLUSH_INTERVAL);
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
received = rx.recv() => match received {
|
||||
Some(event) => {
|
||||
batch.push(event);
|
||||
if batch.len() >= LOG_BATCH_SIZE {
|
||||
flush_logs(&client, &mut batch).await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
flush_logs(&client, &mut batch).await;
|
||||
return;
|
||||
}
|
||||
},
|
||||
_ = ticker.tick() => flush_logs(&client, &mut batch).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn flush_logs(client: &reqwest::Client, batch: &mut Vec<LogEvent>) {
|
||||
if batch.is_empty() {
|
||||
return;
|
||||
}
|
||||
let payload = logs_payload(batch.iter());
|
||||
batch.clear();
|
||||
post(client, &CONFIG.get().expect("config set before export").logs_url, &payload).await;
|
||||
}
|
||||
|
||||
async fn post(client: &reqwest::Client, url: &str, payload: &Value) {
|
||||
// Telemetry must never disturb the app; a lost batch is acceptable.
|
||||
let _ = client.post(url).json(payload).send().await;
|
||||
}
|
||||
|
||||
async fn run_metrics_exporter() {
|
||||
let client = match reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(_) => return,
|
||||
};
|
||||
let Ok(pid) = sysinfo::get_current_pid() else {
|
||||
return;
|
||||
};
|
||||
let cpu_count = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(1) as f64;
|
||||
let mut sys = sysinfo::System::new();
|
||||
let mut ticker = tokio::time::interval(METRICS_INTERVAL);
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
// The first tick fires immediately and only primes the CPU counters —
|
||||
// sysinfo needs two samples before cpu_usage() is meaningful.
|
||||
sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
|
||||
ticker.tick().await;
|
||||
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
sys.refresh_memory();
|
||||
sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
|
||||
let Some(process) = sys.process(pid) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let time = now_unix_nanos().to_string();
|
||||
let metrics = json!([
|
||||
gauge("process.memory.usage", "By", json!({"asInt": process.memory().to_string()}), &time),
|
||||
gauge("process.cpu.utilization", "1", json!({"asDouble": f64::from(process.cpu_usage()) / (100.0 * cpu_count)}), &time),
|
||||
gauge("system.memory.utilization", "1", json!({"asDouble": sys.used_memory() as f64 / sys.total_memory().max(1) as f64}), &time),
|
||||
]);
|
||||
let payload = json!({
|
||||
"resourceMetrics": [{
|
||||
"resource": resource(),
|
||||
"scopeMetrics": [{ "scope": {"name": "gitty"}, "metrics": metrics }]
|
||||
}]
|
||||
});
|
||||
post(&client, &CONFIG.get().expect("config set before export").metrics_url, &payload).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn gauge(name: &str, unit: &str, value: Value, time_unix_nano: &str) -> Value {
|
||||
let mut point = json!({"timeUnixNano": time_unix_nano});
|
||||
point
|
||||
.as_object_mut()
|
||||
.expect("point is an object")
|
||||
.extend(value.as_object().cloned().unwrap_or_default());
|
||||
json!({"name": name, "unit": unit, "gauge": {"dataPoints": [point]}})
|
||||
}
|
||||
|
||||
fn logs_payload<'a>(events: impl Iterator<Item = &'a LogEvent>) -> Value {
|
||||
let records: Vec<Value> = events
|
||||
.map(|event| {
|
||||
let attributes: Vec<Value> = event
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|(key, value)| string_attribute(key, value))
|
||||
.collect();
|
||||
json!({
|
||||
"timeUnixNano": event.time_unix_nano.to_string(),
|
||||
"severityNumber": event.severity.0,
|
||||
"severityText": event.severity.1,
|
||||
"body": {"stringValue": event.body},
|
||||
"attributes": attributes,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
json!({
|
||||
"resourceLogs": [{
|
||||
"resource": resource(),
|
||||
"scopeLogs": [{ "scope": {"name": "gitty"}, "logRecords": records }]
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
fn resource() -> Value {
|
||||
let version = CONFIG
|
||||
.get()
|
||||
.map(|c| c.app_version.as_str())
|
||||
.unwrap_or("unknown");
|
||||
json!({
|
||||
"attributes": [
|
||||
string_attribute("service.name", "gitty"),
|
||||
string_attribute("service.version", version),
|
||||
string_attribute("os.type", std::env::consts::OS),
|
||||
string_attribute("host.arch", std::env::consts::ARCH),
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
fn string_attribute(key: &str, value: &str) -> Value {
|
||||
json!({"key": key, "value": {"stringValue": value}})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn logs_payload_matches_otlp_shape() {
|
||||
let event = LogEvent::new(SEVERITY_ERROR, "boom").attribute("log.target", "gitty::git");
|
||||
let payload = logs_payload(std::iter::once(&event));
|
||||
|
||||
let record = &payload["resourceLogs"][0]["scopeLogs"][0]["logRecords"][0];
|
||||
assert_eq!(record["severityNumber"], 17);
|
||||
assert_eq!(record["severityText"], "ERROR");
|
||||
assert_eq!(record["body"]["stringValue"], "boom");
|
||||
assert_eq!(record["attributes"][0]["key"], "log.target");
|
||||
assert_eq!(record["attributes"][0]["value"]["stringValue"], "gitty::git");
|
||||
// OTLP/JSON requires uint64 nanos as a string.
|
||||
assert!(record["timeUnixNano"].is_string());
|
||||
|
||||
let resource_attrs = &payload["resourceLogs"][0]["resource"]["attributes"];
|
||||
assert_eq!(resource_attrs[0]["key"], "service.name");
|
||||
assert_eq!(resource_attrs[0]["value"]["stringValue"], "gitty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gauge_merges_value_into_data_point() {
|
||||
let metric = gauge("process.memory.usage", "By", json!({"asInt": "1024"}), "42");
|
||||
assert_eq!(metric["name"], "process.memory.usage");
|
||||
assert_eq!(metric["unit"], "By");
|
||||
let point = &metric["gauge"]["dataPoints"][0];
|
||||
assert_eq!(point["timeUnixNano"], "42");
|
||||
assert_eq!(point["asInt"], "1024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_bodies_are_truncated_on_char_boundary() {
|
||||
let body = "ä".repeat(MAX_TEXT_LEN);
|
||||
let event = LogEvent::new(SEVERITY_WARN, body);
|
||||
assert!(event.body.len() <= MAX_TEXT_LEN + " …[truncated]".len());
|
||||
assert!(event.body.ends_with("…[truncated]"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
// A rendering bug firing on every frame must not flood the collector, so
|
||||
// duplicate messages are sent once and the session is capped overall.
|
||||
const MAX_REPORTS_PER_SESSION = 25;
|
||||
const seen = new Set<string>();
|
||||
|
||||
function report(message: string, stack?: string, source?: string) {
|
||||
if (seen.size >= MAX_REPORTS_PER_SESSION || seen.has(message)) {
|
||||
return;
|
||||
}
|
||||
seen.add(message);
|
||||
void invoke("report_frontend_error", { message, stack, source }).catch(() => {
|
||||
// Telemetry must never interrupt the Git workflow.
|
||||
});
|
||||
}
|
||||
|
||||
/** Forwards uncaught errors and unhandled promise rejections to SigNoz. */
|
||||
export function installErrorReporting() {
|
||||
window.addEventListener("error", (event) => {
|
||||
const error: unknown = event.error;
|
||||
report(
|
||||
event.message || String(error),
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : undefined,
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
const reason: unknown = event.reason;
|
||||
if (reason instanceof Error) {
|
||||
report(`Unhandled rejection: ${reason.message}`, reason.stack);
|
||||
} else {
|
||||
report(`Unhandled rejection: ${String(reason)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import { mount } from "svelte";
|
||||
|
||||
import App from "./App.svelte";
|
||||
import "./app.css";
|
||||
import { installErrorReporting } from "./lib/telemetry";
|
||||
|
||||
installErrorReporting();
|
||||
|
||||
const THEME_KEY = "gitlite.theme.v1";
|
||||
const target = document.getElementById("app");
|
||||
|
||||
Reference in New Issue
Block a user