Checking in progress, though not fully working as expected
This commit is contained in:
@@ -315,13 +315,7 @@ impl RequestParams {
|
||||
user_workspaces.is_bedrock_enabled(app),
|
||||
geap_binding,
|
||||
);
|
||||
let is_custom_inference_enabled = user_workspaces.is_custom_inference_enabled(app);
|
||||
let custom_model_providers = FeatureFlag::CustomInferenceEndpoints
|
||||
.is_enabled()
|
||||
.then(|| {
|
||||
api_key_manager.custom_model_providers_for_request(is_custom_inference_enabled)
|
||||
})
|
||||
.flatten();
|
||||
let custom_model_providers = None;
|
||||
let custom_model_routers = FeatureFlag::CustomModelRouters.is_enabled().then(|| {
|
||||
LLMPreferences::as_ref(app).custom_model_routers_for_request(
|
||||
&request_input.model_id,
|
||||
|
||||
@@ -214,25 +214,6 @@ pub async fn generate_multi_agent_output(
|
||||
}
|
||||
}
|
||||
|
||||
async fn convert_multi_agent_client_error(
|
||||
error: warp_multi_agent_client::Error,
|
||||
) -> Arc<AIApiError> {
|
||||
let error = match error {
|
||||
warp_multi_agent_client::Error::Authentication(error)
|
||||
| warp_multi_agent_client::Error::AmbientHeaders(error) => AIApiError::Other(error),
|
||||
warp_multi_agent_client::Error::Base64Decode(error) => {
|
||||
AIApiError::Other(anyhow::Error::from(error))
|
||||
}
|
||||
warp_multi_agent_client::Error::ProtobufDecode(error) => {
|
||||
AIApiError::Other(anyhow::Error::from(error))
|
||||
}
|
||||
warp_multi_agent_client::Error::EventSource(error) => {
|
||||
AIApiError::from_stream_error("GenerateMultiAgentOutput", *error).await
|
||||
}
|
||||
};
|
||||
Arc::new(error)
|
||||
}
|
||||
|
||||
fn api_keys_with_warp_credit_fallback_setting(
|
||||
api_keys: Option<api::request::settings::ApiKeys>,
|
||||
allow_use_of_warp_credits: bool,
|
||||
|
||||
+7
-88
@@ -628,14 +628,10 @@ impl LLMPreferences {
|
||||
// RequiresUpgrade models may become usable or unusable.
|
||||
// Also rebuild `custom_llms` so adds/edits/removals to the user's custom endpoints
|
||||
// immediately flow through to the model picker.
|
||||
ctx.subscribe_to_model(
|
||||
&ApiKeyManager::handle(ctx),
|
||||
|me, _, _event: &ApiKeyManagerEvent, ctx| {
|
||||
me.rebuild_custom_llms(ctx);
|
||||
me.reconcile_disabled_model_preferences(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
},
|
||||
);
|
||||
ctx.subscribe_to_model(&ApiKeyManager::handle(ctx), |me, _, _event, ctx| {
|
||||
me.reconcile_disabled_model_preferences(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
});
|
||||
|
||||
// Rebuild custom model routers whenever the local `model_configs/` directory
|
||||
// changes, and reconcile any now-stale local selection.
|
||||
@@ -1400,8 +1396,8 @@ impl LLMPreferences {
|
||||
}
|
||||
|
||||
fn custom_inference_enabled(app: &AppContext) -> bool {
|
||||
FeatureFlag::CustomInferenceEndpoints.is_enabled()
|
||||
&& UserWorkspaces::as_ref(app).is_custom_inference_enabled(app)
|
||||
let _ = app;
|
||||
false
|
||||
}
|
||||
|
||||
/// Resolves a custom model router by its `config_key`/`LLMId`.
|
||||
@@ -1583,84 +1579,7 @@ impl LLMPreferences {
|
||||
/// Reads the user's current `ApiKeyManager.custom_endpoints` and replaces `custom_llms`
|
||||
/// with synthetic `LLMInfo`s. Called on every `ApiKeyManagerEvent::KeysUpdated`, so adds,
|
||||
/// edits, and removals all propagate immediately.
|
||||
fn rebuild_custom_llms(&mut self, app: &AppContext) {
|
||||
self.custom_llms = build_custom_llm_infos(ApiKeyManager::as_ref(app).keys());
|
||||
}
|
||||
|
||||
fn sanitize_disabled_custom_model_preferences(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if Self::custom_inference_enabled(ctx) || self.custom_llms.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let custom_ids: HashSet<_> = self
|
||||
.custom_llms
|
||||
.iter()
|
||||
.map(|info| info.id.clone())
|
||||
.collect();
|
||||
let mut updated_agent_mode = false;
|
||||
let mut updated_coding = false;
|
||||
let mut updated_other = false;
|
||||
|
||||
self.base_llm_for_terminal_view.retain(|_, id| {
|
||||
let keep = !custom_ids.contains(id);
|
||||
updated_agent_mode |= !keep;
|
||||
keep
|
||||
});
|
||||
|
||||
AIExecutionProfilesModel::handle(ctx).update(ctx, |profiles, ctx| {
|
||||
for profile_id in profiles.get_all_profile_ids() {
|
||||
let Some(profile) = profiles.get_profile_by_id(profile_id, ctx) else {
|
||||
continue;
|
||||
};
|
||||
let profile_data = profile.data();
|
||||
|
||||
if profile_data
|
||||
.base_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_base_model(profile_id, None, ctx);
|
||||
profiles.set_context_window_limit(profile_id, None, ctx);
|
||||
updated_agent_mode = true;
|
||||
}
|
||||
if profile_data
|
||||
.coding_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_coding_model(profile_id, None, ctx);
|
||||
updated_coding = true;
|
||||
}
|
||||
if profile_data
|
||||
.cli_agent_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_cli_agent_model(profile_id, None, ctx);
|
||||
updated_other = true;
|
||||
}
|
||||
if profile_data
|
||||
.computer_use_model
|
||||
.as_ref()
|
||||
.is_some_and(|id| custom_ids.contains(id))
|
||||
{
|
||||
profiles.set_computer_use_model(profile_id, None, ctx);
|
||||
updated_other = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if updated_agent_mode {
|
||||
self.trigger_snapshot_save(ctx);
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveAgentModeLLM);
|
||||
}
|
||||
if updated_coding {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedActiveCodingLLM);
|
||||
}
|
||||
if updated_other {
|
||||
ctx.emit(LLMPreferencesEvent::UpdatedAvailableLLMs);
|
||||
}
|
||||
}
|
||||
fn sanitize_disabled_custom_model_preferences(&mut self, _ctx: &mut ModelContext<Self>) {}
|
||||
|
||||
/// Returns the default base model as a fallback.
|
||||
/// Returns `true` if at least one real AI provider model is configured and available.
|
||||
|
||||
@@ -343,14 +343,6 @@ impl AuthManager {
|
||||
|
||||
self.set_needs_reauth(false, ctx);
|
||||
|
||||
// Must be called on the main thread.
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::set_user_id(
|
||||
user.local_id,
|
||||
Some(user.metadata.email.clone()),
|
||||
ctx,
|
||||
);
|
||||
|
||||
ServerApiProvider::handle(ctx).update(ctx, |provider, ctx| {
|
||||
provider.handle_experiments_fetched(server_experiments, ctx);
|
||||
});
|
||||
|
||||
@@ -489,35 +489,6 @@ pub fn render_privacy_settings_toggles<A: Action + Clone + 'static>(
|
||||
)
|
||||
.finish();
|
||||
|
||||
let toggle_crash = actions.toggle_crash_reporting.clone();
|
||||
let crash_reporting_toggle = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
render_privacy_settings_section_header("Send crash reports", appearance).finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
appearance
|
||||
.ui_builder()
|
||||
.switch(handles.crash_reporting_switch.clone())
|
||||
.check(PrivacySettings::as_ref(app).is_crash_reporting_enabled)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(toggle_crash.clone());
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let crash_reporting_description = render_description(
|
||||
appearance,
|
||||
"Crash reporting helps Warp's engineering team understand stability and improve performance.".into(),
|
||||
);
|
||||
|
||||
let toggle_cloud = actions.toggle_cloud_conversation_storage.clone();
|
||||
let cloud_conversation_storage_toggle = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
@@ -576,17 +547,6 @@ pub fn render_privacy_settings_toggles<A: Action + Clone + 'static>(
|
||||
]);
|
||||
}
|
||||
|
||||
if ChannelState::is_crash_reporting_available() {
|
||||
col.add_children(vec![
|
||||
Container::new(crash_reporting_toggle)
|
||||
.with_margin_bottom(AUTH_MODAL_GAP)
|
||||
.finish(),
|
||||
Container::new(crash_reporting_description)
|
||||
.with_margin_bottom(AUTH_MODAL_GAP)
|
||||
.finish(),
|
||||
]);
|
||||
}
|
||||
|
||||
// Hide the cloud conversation storage toggle entirely when AI is disabled:
|
||||
// the setting has no effect without AI, and showing it is confusing.
|
||||
if FeatureFlag::CloudConversations.is_enabled() && is_ai_enabled {
|
||||
|
||||
@@ -122,7 +122,7 @@ fn parse_minidump_cleanup_exit_code(contents_lowercase: &[u8]) -> Option<i32> {
|
||||
}
|
||||
|
||||
/// Checks the autoupdate log file from a previous update attempt.
|
||||
/// Sends telemetry for specific known issues, and sends a Sentry event if errors are found.
|
||||
/// Sends telemetry for specific known issues.
|
||||
/// The log file is renamed after processing to avoid duplicate reports on subsequent launches.
|
||||
pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
|
||||
let log_path = match autoupdate_log_file() {
|
||||
@@ -198,51 +198,7 @@ pub(super) fn check_and_report_update_errors(ctx: &mut AppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
{
|
||||
use sentry::protocol::{Attachment, AttachmentType};
|
||||
|
||||
// Patterns for known benign errors that should not trigger Sentry reporting.
|
||||
const IGNOREABLE_ERRORS: &[&[u8]] = &[
|
||||
// User running out of disk space is not an error we need concern ourselves with.
|
||||
// This message occurs after "An error occurred while trying to copy a file:"
|
||||
b"there is not enough space on the disk",
|
||||
// Recent Inno Setup versions try to enable a security feature which is unavailable on
|
||||
// Windows 10 versions prior to 22H2 and this call fails. The failure is benign.
|
||||
b"setprocessmitigationpolicy failed with error code 87",
|
||||
// Bundled skill files whose names contain "error" appear in "Dest filename:" log lines
|
||||
// and produce false positives.
|
||||
b"error-codes.md",
|
||||
b"error-recovery.md",
|
||||
];
|
||||
|
||||
let mut error_count = memchr::memmem::find_iter(&contents_lowercase, b"error").count();
|
||||
|
||||
for pattern in IGNOREABLE_ERRORS {
|
||||
let ignoreable_count = memchr::memmem::find_iter(&contents_lowercase, pattern).count();
|
||||
error_count = error_count.saturating_sub(ignoreable_count);
|
||||
}
|
||||
|
||||
if error_count > 0 {
|
||||
log::warn!("Autoupdate log file contains errors; reporting to Sentry");
|
||||
|
||||
let attachment = Attachment {
|
||||
buffer: contents,
|
||||
filename: UPDATE_LOG_FILENAME.to_string(),
|
||||
ty: Some(AttachmentType::Attachment),
|
||||
..Default::default()
|
||||
};
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
scope.add_attachment(attachment);
|
||||
},
|
||||
|| sentry::capture_message("Windows auto-update error", sentry::Level::Error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Rename the log file to avoid duplicate reports on subsequent launches.
|
||||
// We keep the file around so the user can still view it or attach it to a GitHub issue.
|
||||
// Rename the log file after processing. Keep it around so the user can inspect it.
|
||||
let reported_path = log_path.with_extension("log.reported");
|
||||
if let Err(e) = fs::rename(&log_path, &reported_path) {
|
||||
log::warn!("Failed to rename autoupdate log file after reporting: {e:#}");
|
||||
|
||||
@@ -42,7 +42,6 @@ pub fn main() -> Result<()> {
|
||||
workload_audience_url: None,
|
||||
},
|
||||
telemetry_config: None,
|
||||
crash_reporting_config: None,
|
||||
autoupdate_config: None,
|
||||
mcp_static_config: None,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ fn main() -> Result<()> {
|
||||
server_config: WarpServerConfig::production(),
|
||||
oz_config: OzConfig::production(),
|
||||
telemetry_config: None,
|
||||
crash_reporting_config: None,
|
||||
autoupdate_config: None,
|
||||
mcp_static_config: None,
|
||||
},
|
||||
|
||||
@@ -19,7 +19,6 @@ lazy_static! {
|
||||
static ref IS_CRASH_RECOVERY_PROCESS_RUNNING: RwLock<bool> = RwLock::new(false);
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "crash_reporting"), allow(dead_code))]
|
||||
pub fn is_crash_recovery_process_running() -> bool {
|
||||
*IS_CRASH_RECOVERY_PROCESS_RUNNING.read()
|
||||
}
|
||||
@@ -108,10 +107,6 @@ impl CrashRecoveryProcess {
|
||||
"Failed to render a frame {NUM_DRAW_ERRORS_BEFORE_EXITING} times in a row; exiting..."
|
||||
);
|
||||
|
||||
// Uninitialize sentry (ensuring any remaining events get flushed) before hard exiting.
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::uninit_sentry();
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use crate::crash_reporting::VirtualEnvironment;
|
||||
|
||||
/// Returns what virtualized environment Warp is running in, if any.
|
||||
pub fn get_virtualized_environment() -> Option<VirtualEnvironment> {
|
||||
if let Ok(output) = command::blocking::Command::new("systemd-detect-virt").output() {
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
} else {
|
||||
let value = std::str::from_utf8(&output.stdout).ok()?;
|
||||
if value == "none" {
|
||||
return None;
|
||||
}
|
||||
return Some(VirtualEnvironment {
|
||||
name: value.to_owned(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if command::wsl::is_wsl() {
|
||||
return Some(VirtualEnvironment {
|
||||
name: "wsl".to_owned(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use objc2::rc::autoreleasepool;
|
||||
use objc2_foundation::NSString;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Functions implemented in objC files.
|
||||
extern "C" {
|
||||
fn startSentry(
|
||||
sentryUrl: &NSString,
|
||||
environment: &NSString,
|
||||
version: &NSString,
|
||||
isDogfood: bool,
|
||||
);
|
||||
fn stopSentry();
|
||||
#[allow(dead_code)] // Only gets called when built in debug mode.
|
||||
fn crashSentry();
|
||||
fn setUser(userId: &NSString);
|
||||
fn recordBreadcrumb(
|
||||
message: &NSString,
|
||||
category: &NSString,
|
||||
level: &NSString,
|
||||
seconds_since_epoch: f64,
|
||||
);
|
||||
fn setTag(key: &NSString, value: &NSString);
|
||||
}
|
||||
|
||||
pub fn init_cocoa_sentry() {
|
||||
let endpoint = ChannelState::sentry_url();
|
||||
let environment = super::get_environment();
|
||||
|
||||
log::info!("Initializing Sentry for cocoa app with endpoint {endpoint}");
|
||||
// This runs during early init from `init_sentry`, before the AppKit event
|
||||
// loop drains its ambient pool, so open a local pool to bound the bridge
|
||||
// NSStrings.
|
||||
autoreleasepool(|_| {
|
||||
let dsn = NSString::from_str(endpoint.as_ref());
|
||||
let environment_name: &str = environment.as_ref();
|
||||
let environment = NSString::from_str(environment_name);
|
||||
let release = NSString::from_str(release_version());
|
||||
unsafe {
|
||||
startSentry(
|
||||
&dsn,
|
||||
&environment,
|
||||
&release,
|
||||
ChannelState::channel().is_dogfood(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn uninit_cocoa_sentry() {
|
||||
unsafe {
|
||||
stopSentry();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn crash() {
|
||||
unsafe {
|
||||
crashSentry();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user_id(user_id: &str) {
|
||||
// Invoked from `set_optional_user_information` on auth state changes and
|
||||
// init, whose thread of origin varies, so open a local pool to bound the
|
||||
// bridge NSString.
|
||||
autoreleasepool(|_| {
|
||||
let user_id = NSString::from_str(user_id);
|
||||
unsafe {
|
||||
setUser(&user_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn forward_breadcrumb(rust_breadcrumb: &sentry::Breadcrumb) {
|
||||
let message = rust_breadcrumb.message.as_deref().unwrap_or("");
|
||||
let category = rust_breadcrumb.category.as_deref().unwrap_or("");
|
||||
let level = rust_breadcrumb.level.to_string();
|
||||
let unix_timestamp = rust_breadcrumb
|
||||
.timestamp
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.map_or(0., |n| n.as_secs_f64());
|
||||
// Runs on whichever Rust thread emitted the breadcrumb (Sentry's
|
||||
// `before_breadcrumb`), which has no ambient pool, so bound the bridge
|
||||
// NSStrings in a local pool.
|
||||
autoreleasepool(|_| {
|
||||
let message = NSString::from_str(message);
|
||||
let category = NSString::from_str(category);
|
||||
let level = NSString::from_str(level.as_str());
|
||||
unsafe {
|
||||
recordBreadcrumb(&message, &category, &level, unix_timestamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_tag(key: &str, value: &str) {
|
||||
// Called from `init_cocoa_sentry`'s tag loop and the `set_tag` wrapper in
|
||||
// `mod.rs` on Rust threads, so open a local pool to bound the bridge
|
||||
// NSStrings.
|
||||
autoreleasepool(|_| {
|
||||
let key = NSString::from_str(key);
|
||||
let value = NSString::from_str(value);
|
||||
unsafe {
|
||||
setTag(&key, &value);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,617 +0,0 @@
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mod mac;
|
||||
#[cfg(linux_or_windows)]
|
||||
mod sentry_minidump;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
use galaxy_core::channel::Channel;
|
||||
use galaxyui::r#async::block_on;
|
||||
use galaxyui::rendering::GPUDeviceInfo;
|
||||
use galaxyui::windowing::state::ApplicationStage;
|
||||
use galaxyui::windowing::{self, StateEvent, WindowManager};
|
||||
use galaxyui::{AppContext, SingletonEntity};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use regex::Regex;
|
||||
use sentry::{ClientInitGuard, IntoDsn, SessionMode};
|
||||
#[cfg(linux_or_windows)]
|
||||
pub use sentry_minidump::run_server as run_minidump_server;
|
||||
use warp_server_auth::anonymous_id::get_or_create_anonymous_id;
|
||||
|
||||
use crate::antivirus::{AntivirusInfo, AntivirusInfoEvent};
|
||||
use crate::auth::{AuthStateProvider, UserUid};
|
||||
use crate::channel::ChannelState;
|
||||
use crate::features::FeatureFlag;
|
||||
use crate::settings::{PrivacySettings, PrivacySettingsChangedEvent};
|
||||
|
||||
lazy_static! {
|
||||
/// The RAII guard returned by the call to initialize the Rust Sentry client must be kept in
|
||||
/// scope. When it is destroyed, Sentry monitoring ceases.
|
||||
static ref RUST_SENTRY_CLIENT_GUARD: Mutex<RustSentryClientGuard> =
|
||||
Mutex::new(RustSentryClientGuard::Uninitialized);
|
||||
|
||||
/// A map from sensitive error messages to "scrubbed" error messages.
|
||||
static ref ERROR_MESSAGES_TO_SCRUB: Vec<(Regex, &'static str)> = vec![
|
||||
// The following are panic messages for invalid string slicing.
|
||||
// See here for source: https://cs.github.com/rust-lang/rust/blob/9c0bc3028a575eece6d4e8fbc6624cb95b9c9893/library/core/src/str/mod.rs?q=%22byte+index+%22+repo%3Arust-lang%2Frust#L100.
|
||||
(Regex::new(r"byte index .+ is out of bounds.+").unwrap(), "byte index is out of bounds"),
|
||||
(Regex::new(r"byte index .+ is not a char boundary.+").unwrap(), "byte index is not a char boundary"),
|
||||
(Regex::new(r"begin <= end .+ when slicing.+").unwrap(), "begin <= end when slicing"),
|
||||
];
|
||||
|
||||
/// The current [`ApplicationStage`] of the application. Used when reporting the
|
||||
/// `warp.application_stage` tag to Sentry.
|
||||
static ref APPLICATION_LIFECYCLE_STAGE: RwLock<ApplicationStage> = RwLock::new(ApplicationStage::Starting);
|
||||
|
||||
/// The set of tags that we want to attach to all Sentry reports.
|
||||
static ref TAGS: RwLock<HashMap<String, String>> = Default::default();
|
||||
}
|
||||
|
||||
/// Sets a kv-pair as a Sentry tag for the rest of the application's lifetime.
|
||||
pub(crate) fn set_tag<'a, 'b>(key: impl Into<Cow<'a, str>>, value: impl Into<Cow<'b, str>>) {
|
||||
set_tag_internal(key.into(), value.into());
|
||||
}
|
||||
|
||||
/// Non-generic internal implementation of [`set_tag`].
|
||||
fn set_tag_internal(key: Cow<'_, str>, value: Cow<'_, str>) {
|
||||
// Avoid setting tags with empty values, as Sentry doesn't allow them.
|
||||
if value.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_tag(key.clone().into_owned(), value.clone().into_owned());
|
||||
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::set_tag(key.as_ref(), value.as_ref());
|
||||
|
||||
TAGS.write().insert(key.into_owned(), value.into_owned());
|
||||
}
|
||||
|
||||
/// Sets the [`GPUDeviceInfo`] of the last opened window for use in logging as a Sentry tag.
|
||||
pub(crate) fn set_gpu_device_info(gpu_device_info: GPUDeviceInfo) {
|
||||
for (key, value) in gpu_device_info.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`AntivirusInfo`] for use in logging as a Sentry tag.
|
||||
/// Only reports the first detected antivirus product.
|
||||
pub fn set_antivirus_info(antivirus_info: &AntivirusInfo) {
|
||||
for (key, value) in antivirus_info.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current application lifecycle stage, for use in logging as a Sentry tag.
|
||||
fn set_lifecycle_stage(stage: ApplicationStage) {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_tags_from(&stage);
|
||||
|
||||
*APPLICATION_LIFECYCLE_STAGE.write() = stage;
|
||||
}
|
||||
|
||||
/// Sets the detected virtual environment info, for use in logging as a Sentry
|
||||
/// tag.
|
||||
fn set_virtual_environment(env: Option<VirtualEnvironment>) {
|
||||
for (key, value) in env.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_windowing_system(windowing_system: Option<windowing::System>) {
|
||||
for (key, value) in windowing_system.to_sentry_tags() {
|
||||
set_tag(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if crash reporting is currently enabled.
|
||||
fn is_crash_reporting_enabled(ctx: &mut AppContext) -> bool {
|
||||
PrivacySettings::handle(ctx)
|
||||
.as_ref(ctx)
|
||||
.is_crash_reporting_enabled
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct CrashRecoveryMetadata {
|
||||
/// Whether the Sentry event was previously _unhandled_.
|
||||
was_unhandled_event: bool,
|
||||
/// Whether the crash recovery process is currently running, indicating that an unhandled event
|
||||
/// should actually be marked as handled.
|
||||
is_crash_recovery_process_running: bool,
|
||||
}
|
||||
|
||||
impl CrashRecoveryMetadata {
|
||||
#[cfg(enable_crash_recovery)]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
was_unhandled_event: false,
|
||||
is_crash_recovery_process_running:
|
||||
crate::crash_recovery::is_crash_recovery_process_running(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(enable_crash_recovery))]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
was_unhandled_event: false,
|
||||
is_crash_recovery_process_running: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn was_unhandled_event(&mut self) {
|
||||
self.was_unhandled_event = true;
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for CrashRecoveryMetadata {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
#[cfg(enable_crash_recovery)]
|
||||
{
|
||||
/// Converts a `bool` that is meant to be a Sentry value to it's string representation.
|
||||
/// Sentry uses `yes` or `no` as the value for booleans, so we follow that convention.
|
||||
fn bool_to_sentry_value(value: bool) -> String {
|
||||
let sentry_value = if value { "yes" } else { "no" };
|
||||
sentry_value.into()
|
||||
}
|
||||
|
||||
[
|
||||
(
|
||||
"warp.crash_recovery_process.running",
|
||||
bool_to_sentry_value(self.is_crash_recovery_process_running),
|
||||
),
|
||||
(
|
||||
"warp.handled_by_crash_recovery_process",
|
||||
bool_to_sentry_value(self.was_unhandled_event),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(not(enable_crash_recovery))]
|
||||
std::iter::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the crash reporting subsystem. Returns whether or not crash
|
||||
/// reporting is active.
|
||||
pub(crate) fn init(ctx: &mut AppContext) -> bool {
|
||||
if !FeatureFlag::CrashReporting.is_enabled() {
|
||||
log::info!("Crash reporting FeatureFlag is disabled; not initializing sentry.");
|
||||
return false;
|
||||
}
|
||||
|
||||
let window_manager = WindowManager::handle(ctx);
|
||||
ctx.subscribe_to_model(&window_manager, |_, event, _| match event {
|
||||
StateEvent::ValueChanged { current, previous } => {
|
||||
if current.stage != previous.stage {
|
||||
set_lifecycle_stage(current.stage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let antivirus_info = AntivirusInfo::handle(ctx);
|
||||
ctx.subscribe_to_model(&antivirus_info, |antivirus_info, event, ctx| match event {
|
||||
AntivirusInfoEvent::ScannedComplete => {
|
||||
let antivirus_info = antivirus_info.as_ref(ctx);
|
||||
set_antivirus_info(antivirus_info);
|
||||
}
|
||||
});
|
||||
|
||||
let is_crash_reporting_enabled = is_crash_reporting_enabled(ctx);
|
||||
|
||||
if is_crash_reporting_enabled {
|
||||
AuthStateProvider::handle(ctx).update(ctx, |auth_state_provider, ctx| {
|
||||
init_sentry(
|
||||
auth_state_provider.get().user_id(),
|
||||
auth_state_provider.get().user_email(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
log::info!("Crash reporting setting is disabled; not initializing sentry.");
|
||||
}
|
||||
|
||||
set_windowing_system(ctx.windows().windowing_system());
|
||||
|
||||
let privacy_settings = PrivacySettings::handle(ctx);
|
||||
ctx.subscribe_to_model(&privacy_settings, |_, event, ctx| {
|
||||
if let &PrivacySettingsChangedEvent::UpdateIsCrashReportingEnabled { new_value, .. } = event
|
||||
{
|
||||
if new_value {
|
||||
AuthStateProvider::handle(ctx).update(ctx, |auth_state_provider, ctx| {
|
||||
init_sentry(
|
||||
auth_state_provider.get().user_id(),
|
||||
auth_state_provider.get().user_email(),
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
uninit_sentry();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Having initialized the SDK above, we can now set the initial value of
|
||||
// some tags.
|
||||
set_lifecycle_stage(window_manager.as_ref(ctx).stage());
|
||||
init_virtual_environment_tag(ctx);
|
||||
|
||||
is_crash_reporting_enabled
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum RustSentryClientGuard {
|
||||
#[default]
|
||||
Uninitialized,
|
||||
Initialized {
|
||||
_guard: ClientInitGuard,
|
||||
},
|
||||
}
|
||||
|
||||
/// Returns the environment used when reporting events to Sentry.
|
||||
/// This is the name of the operating system followed by the channel name (i.e. "linux_dev_release").
|
||||
fn get_environment() -> Cow<'static, str> {
|
||||
let channel = ChannelState::channel();
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "macos")] {
|
||||
let operating_system = "mac";
|
||||
} else if #[cfg(target_os = "linux")] {
|
||||
let operating_system = "linux";
|
||||
}
|
||||
else if #[cfg(target_os = "windows")] {
|
||||
let operating_system = "windows";
|
||||
} else {
|
||||
let operating_system = "";
|
||||
}
|
||||
};
|
||||
|
||||
let base_environment_name = match channel {
|
||||
Channel::Stable => "stable_release",
|
||||
Channel::Preview => "preview_release",
|
||||
Channel::Local => "local",
|
||||
Channel::Integration => "integration_test",
|
||||
Channel::Dev => "dev_release",
|
||||
Channel::Oss => "oss_release",
|
||||
};
|
||||
|
||||
if operating_system.is_empty() {
|
||||
base_environment_name.into()
|
||||
} else {
|
||||
format!("{operating_system}_{base_environment_name}").into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes Rust and Cocoa Sentry, which hooks into the panic handler for the rust app and
|
||||
/// uncaught exception handler of the mac runtime, respectively.
|
||||
///
|
||||
/// This must be called from the main thread to capture panics/crashes across the entire
|
||||
/// application.
|
||||
fn init_sentry(user_id: Option<UserUid>, email: Option<String>, ctx: &mut AppContext) {
|
||||
let key = release_version();
|
||||
|
||||
let environment = Some(get_environment());
|
||||
|
||||
log::info!("Initializing crash reporting {environment:?} with tag {key:?}...");
|
||||
|
||||
fn before_breadcrumb(crumb: sentry::Breadcrumb) -> Option<sentry::Breadcrumb> {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::forward_breadcrumb(crumb.clone());
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::forward_breadcrumb(&crumb);
|
||||
|
||||
Some(crumb)
|
||||
}
|
||||
|
||||
/// We scrub text we send to Sentry so that we don't leak user input into
|
||||
/// crash reports.
|
||||
fn scrub_message(message: &mut String) {
|
||||
for (regex, replacement) in ERROR_MESSAGES_TO_SCRUB.iter() {
|
||||
if regex.is_match(message) {
|
||||
*message = format!("(REDACTED) {replacement}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut sentry_options = sentry_client_options();
|
||||
sentry_options.before_breadcrumb = Some(Arc::new(Box::new(before_breadcrumb)));
|
||||
sentry_options.before_send = Some(Arc::new(move |mut event| {
|
||||
let mut crash_recovery_metadata = CrashRecoveryMetadata::new();
|
||||
|
||||
for exception in event.exception.iter_mut() {
|
||||
exception.value.as_mut().map(scrub_message);
|
||||
|
||||
// If the crash recovery process is running, mark any exception as "handled".
|
||||
// The crash recovery process will attempt to the handle that crash, if
|
||||
// we crash when handling we'll report that as an unhandled event to sentry.
|
||||
if crash_recovery_metadata.is_crash_recovery_process_running {
|
||||
if let Some(mechanism) = exception.mechanism.as_mut() {
|
||||
if let Some(false) = mechanism.handled {
|
||||
crash_recovery_metadata.was_unhandled_event();
|
||||
}
|
||||
|
||||
mechanism.handled = Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (k, v) in APPLICATION_LIFECYCLE_STAGE.read().to_sentry_tags() {
|
||||
event.tags.insert(k.to_string(), v);
|
||||
}
|
||||
for (k, v) in TAGS.read().iter() {
|
||||
event.tags.insert(k.clone(), v.clone());
|
||||
}
|
||||
|
||||
Some(event)
|
||||
}));
|
||||
|
||||
*RUST_SENTRY_CLIENT_GUARD.lock() = RustSentryClientGuard::Initialized {
|
||||
_guard: sentry::init(sentry_options),
|
||||
};
|
||||
|
||||
// Initialize the appropriate native Sentry SDK.
|
||||
#[cfg(enable_crash_recovery)]
|
||||
{
|
||||
use crate::crash_recovery::{is_crash_recovery_process_running, CrashRecovery};
|
||||
|
||||
// If the crash recovery process is running, defer initialization of Sentry native until the
|
||||
// crash recovery process is torn down. Unlike Sentry Rust, we can't easily mark events as
|
||||
// handled before they are sent to Sentry. Instead, we defer initialization to avoid
|
||||
// erroneously reporting crashes when they would be successfully handled by the crash
|
||||
// recovery process.
|
||||
if is_crash_recovery_process_running() {
|
||||
ctx.subscribe_to_model(&CrashRecovery::handle(ctx), |_handle, event, ctx| {
|
||||
if matches!(
|
||||
event,
|
||||
crate::crash_recovery::Event::CrashRecoveryProcessTornDown
|
||||
) {
|
||||
log::info!("Initializing Sentry native");
|
||||
sentry_minidump::init();
|
||||
|
||||
let auth_state_provider = crate::AuthStateProvider::handle(ctx).as_ref(ctx);
|
||||
let auth_state = auth_state_provider.get();
|
||||
let user_id = auth_state.user_id();
|
||||
let email = auth_state.user_email();
|
||||
set_optional_user_information(user_id, email, ctx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sentry_minidump::init()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if FeatureFlag::CocoaSentry.is_enabled() {
|
||||
init_cocoa_sentry();
|
||||
}
|
||||
|
||||
set_optional_user_information(user_id, email, ctx);
|
||||
}
|
||||
|
||||
/// Baseline Sentry client options.
|
||||
fn sentry_client_options() -> sentry::ClientOptions {
|
||||
sentry::ClientOptions {
|
||||
dsn: ChannelState::sentry_url()
|
||||
.into_dsn()
|
||||
.expect("Invalid Sentry DSN"),
|
||||
|
||||
release: Some(release_version().into()),
|
||||
environment: Some(get_environment()),
|
||||
auto_session_tracking: true,
|
||||
session_mode: SessionMode::Application,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the Rust Sentry client is currently initialized.
|
||||
pub(crate) fn is_initialized() -> bool {
|
||||
matches!(
|
||||
&*RUST_SENTRY_CLIENT_GUARD.lock(),
|
||||
RustSentryClientGuard::Initialized { .. }
|
||||
)
|
||||
}
|
||||
|
||||
/// Uninitializes sentry, effectively ending reporting on crashes and errors.
|
||||
pub fn uninit_sentry() {
|
||||
// Take the client guard out of the mutex, replacing it with
|
||||
// `Uninitialized`.
|
||||
let client_guard = std::mem::take(RUST_SENTRY_CLIENT_GUARD.lock().deref_mut());
|
||||
if matches!(client_guard, RustSentryClientGuard::Initialized { .. }) {
|
||||
log::info!("Uninitializing crash reporting...");
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::uninit();
|
||||
#[cfg(target_os = "macos")]
|
||||
if FeatureFlag::CocoaSentry.is_enabled() {
|
||||
uninit_cocoa_sentry();
|
||||
}
|
||||
|
||||
// Drop the client guard, uninitializing the Sentry Rust SDK.
|
||||
std::mem::drop(client_guard);
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes sentry hooking into the uncaught exception handler of the mac runtime
|
||||
/// which allows us to catch errors within obj-c.
|
||||
pub fn init_cocoa_sentry() {
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
{
|
||||
mac::init_cocoa_sentry();
|
||||
|
||||
for (k, v) in TAGS.read().iter() {
|
||||
mac::set_tag(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uninit_cocoa_sentry() {
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::uninit_cocoa_sentry();
|
||||
}
|
||||
|
||||
pub fn crash() {
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::crash();
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::crash();
|
||||
}
|
||||
|
||||
/// Sets the user id if `Some`, otherwise sets the current user ID to be an anonymous ID indicating
|
||||
/// the user hasn't logged in yet.
|
||||
fn set_optional_user_information(
|
||||
user_id: Option<UserUid>,
|
||||
email: Option<String>,
|
||||
ctx: &mut AppContext,
|
||||
) {
|
||||
let user_id = user_id.map(|uid| uid.as_string()).unwrap_or_else(|| {
|
||||
// If the user isn't signed in, set an anonymous ID. This allows us to
|
||||
// compute more accurate crash-free user metrics.
|
||||
let anonymous_id = get_or_create_anonymous_id(ctx);
|
||||
format!("anon.{anonymous_id}")
|
||||
});
|
||||
// Only send along emails if we're on WarpDev.
|
||||
// We try to keep PII out of Sentry as much as possible.
|
||||
let email = if ChannelState::channel() == Channel::Dev {
|
||||
email
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Set user for Rust sentry.
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_user(Some(sentry::User {
|
||||
id: Some(user_id.clone()),
|
||||
email,
|
||||
ip_address: None,
|
||||
username: None,
|
||||
other: BTreeMap::new(),
|
||||
}));
|
||||
});
|
||||
|
||||
#[cfg(linux_or_windows)]
|
||||
sentry_minidump::set_user_id(user_id.as_str());
|
||||
#[cfg(all(target_os = "macos", feature = "cocoa_sentry"))]
|
||||
mac::set_user_id(user_id.as_str());
|
||||
}
|
||||
|
||||
pub fn set_user_id(user_id: UserUid, email: Option<String>, ctx: &mut AppContext) {
|
||||
// On macOS, Sentry will error if we try to set a user without initializing the SDK.
|
||||
// If crash reporting was disabled, but the user enables it later, we'll set user info as part of initialization.
|
||||
if matches!(
|
||||
&*RUST_SENTRY_CLIENT_GUARD.lock(),
|
||||
RustSentryClientGuard::Initialized { .. }
|
||||
) {
|
||||
set_optional_user_information(Some(user_id), email, ctx);
|
||||
} else {
|
||||
log::info!("Sentry is not initialized; not setting Sentry user info");
|
||||
}
|
||||
}
|
||||
|
||||
fn release_version() -> &'static str {
|
||||
ChannelState::app_version().unwrap_or("<no tag>")
|
||||
}
|
||||
|
||||
/// Sets the warp.client_type Sentry tag.
|
||||
pub fn set_client_type_tag(client_id: &str) {
|
||||
set_tag("warp.client_type", client_id);
|
||||
}
|
||||
|
||||
/// Initializes the warp.virtual_env Sentry tag group.
|
||||
fn init_virtual_environment_tag(ctx: &mut AppContext) {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
|
||||
// Compute the virtual environment in a background thread, as we don't want
|
||||
// to block application startup at all.
|
||||
std::thread::spawn(move || {
|
||||
let virt_env = VirtualEnvironment::detect();
|
||||
let _ = block_on(tx.send(virt_env));
|
||||
});
|
||||
// Once we've computed the value, we want to update the primary Sentry hub,
|
||||
// which means calling `set_virtual_environment` from the main thread.
|
||||
ctx.foreground_executor()
|
||||
.spawn(async move {
|
||||
if let Ok(virt_env) = rx.recv().await {
|
||||
set_virtual_environment(virt_env);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Represents a virtualized environment that the operating system is running
|
||||
/// under.
|
||||
#[derive(Clone)]
|
||||
struct VirtualEnvironment {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl VirtualEnvironment {
|
||||
/// Detects the current virtual environment, if any.
|
||||
fn detect() -> Option<Self> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
linux::get_virtualized_environment()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ToSentryTags {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)>;
|
||||
}
|
||||
|
||||
impl ToSentryTags for ApplicationStage {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[("warp.application_stage", self.to_string())]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for GPUDeviceInfo {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[
|
||||
("warp.gpu.device.name", self.device_name.to_string()),
|
||||
("warp.gpu.device.type", self.device_type.to_string()),
|
||||
("warp.gpu.backend", self.backend.to_string()),
|
||||
("warp.gpu.driver.name", self.driver_name.to_string()),
|
||||
("warp.gpu.driver.info", self.driver_info.to_string()),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for Option<VirtualEnvironment> {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
let env = self.clone();
|
||||
[(
|
||||
"warp.virtual_env.name",
|
||||
env.map(|env| env.name).unwrap_or_else(|| "none".to_owned()),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for Option<windowing::System> {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[(
|
||||
"warp.window.system",
|
||||
self.as_ref()
|
||||
.map(|windowing_system| windowing_system.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_owned()),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSentryTags for &AntivirusInfo {
|
||||
fn to_sentry_tags(&self) -> impl IntoIterator<Item = (&str, String)> {
|
||||
[(
|
||||
"warp.window.antivirus.name",
|
||||
self.get().unwrap_or("none").into(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
@@ -1,442 +0,0 @@
|
||||
//! Native crash reporting adapter that uses the [`minidumper`] crate with Sentry. This allows us
|
||||
//! to capture and report application crashes due to Unix signals like SIGSEGV (segfault)
|
||||
//! or Windows exceptions [https://learn.microsoft.com/en-us/windows/win32/debug/structured-exception-handling].
|
||||
//!
|
||||
//! This is inspired by [`sentry-rust-minidump`](https://github.com/timfish/sentry-rust-minidump),
|
||||
//! with a few important changes:
|
||||
//! * Support for starting and stopping the crash-reporting process, since users can toggle crash reporting at runtime
|
||||
//! * Startup via our command-line parsing, rather than a separate hook
|
||||
//! * Use of anonymous, temporary crash dump files, to ensure they're cleaned up
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read as _, Seek as _, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use command::blocking::Command;
|
||||
use crash_handler::{CrashContext, CrashHandler};
|
||||
use galaxy_core::report_error;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use sentry::protocol::{Attachment, AttachmentType};
|
||||
use sentry::{Breadcrumb, Level};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::ToSentryTags;
|
||||
|
||||
lazy_static! {
|
||||
static ref GUARD: Mutex<Option<MinidumpGuard>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
/// The minidump child process will exit if it doesn't receive a message after some time. This
|
||||
/// ensures that if the parent process exits without cleaning it up, the child won't linger
|
||||
/// forever. We ping the child every `PING_INTERVAL` to make sure it doesn't quit while the
|
||||
/// parent (this process) is running.
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Initialize the minidump reporter.
|
||||
pub fn init() {
|
||||
let mut global_guard = GUARD.lock();
|
||||
|
||||
match MinidumpGuard::start() {
|
||||
Ok(guard) => {
|
||||
*global_guard = Some(guard);
|
||||
}
|
||||
Err(err) => {
|
||||
report_error!(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uninitialize the minidump reporter.
|
||||
pub fn uninit() {
|
||||
let maybe_guard = { GUARD.lock().take() };
|
||||
// Ensure we drop the `MinidumpGuard` after releasing the GUARD mutex. If there's an
|
||||
// error stopping the server, we should log it as a Sentry breadcrumb in the Warp
|
||||
// process, but not forward the breadcrumb to the server process.
|
||||
std::mem::drop(maybe_guard);
|
||||
}
|
||||
|
||||
/// Set a tag to include in minidump crash reports.
|
||||
pub fn set_tag(key: String, value: String) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.set_tags(HashMap::from([(key, value)]));
|
||||
}
|
||||
}
|
||||
|
||||
/// Set tags to include in minidump crash reports, using a type that implements [`ToSentryTags`].
|
||||
pub fn set_tags_from<T: ToSentryTags>(tags: &T) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
let tags = tags
|
||||
.to_sentry_tags()
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
guard.set_tags(tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the user id to include in minidump crash reports.
|
||||
pub fn set_user_id(user_id: &str) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.set_user_id(user_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward a breadcrumb to attach to minidump crash reports.
|
||||
pub fn forward_breadcrumb(breadcrumb: Breadcrumb) {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.add_breadcrumb(breadcrumb);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a crash report via minidump. On certain platforms, this will produce an error report
|
||||
/// without actually crashing the process.
|
||||
pub fn crash() {
|
||||
let global_guard = GUARD.lock();
|
||||
if let Some(guard) = global_guard.as_ref() {
|
||||
guard.crash();
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle for minidump state that must be kept in scope while crash reporting is enabled.
|
||||
pub struct MinidumpGuard {
|
||||
child: process::Child,
|
||||
client: Arc<minidumper::Client>,
|
||||
crash_handler: CrashHandler,
|
||||
}
|
||||
|
||||
/// Run the minidump server process.
|
||||
pub fn run_server(socket_path: &Path) -> anyhow::Result<()> {
|
||||
// For troubleshooting, attempt to log from the minidump server. There's not much we can really
|
||||
// do if crash reporting fails, so creating the log file itself is best-effort.
|
||||
let log_dir = galaxy_core::paths::state_dir().join(galaxy_core::paths::WARP_LOGS_DIR);
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
let log_path = log_dir.join("warp-minidump.log");
|
||||
let log_target = File::create(log_path)
|
||||
.map(|file| env_logger::Target::Pipe(Box::new(file)))
|
||||
.unwrap_or_else(|_| env_logger::Target::Stdout);
|
||||
env_logger::builder()
|
||||
.parse_default_env()
|
||||
.target(log_target)
|
||||
.init();
|
||||
|
||||
let _guard = sentry::init(super::sentry_client_options());
|
||||
|
||||
struct Handler {
|
||||
shutdown: Arc<AtomicBool>,
|
||||
pending_crash_details: Mutex<Option<String>>,
|
||||
}
|
||||
|
||||
impl minidumper::ServerHandler for Handler {
|
||||
fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
|
||||
// Use an anonymous temporary file for crash dumps. The path isn't used when writing a
|
||||
// dump, so we can use an empty value.
|
||||
let file = tempfile::tempfile()?;
|
||||
Ok((file, PathBuf::new()))
|
||||
}
|
||||
|
||||
fn on_minidump_created(
|
||||
&self,
|
||||
result: Result<minidumper::MinidumpBinary, minidumper::Error>,
|
||||
) -> minidumper::LoopAction {
|
||||
if let Err(ref err) = &result {
|
||||
log::warn!("Unable to create minidump file: {err:#}");
|
||||
}
|
||||
|
||||
let crash_details = self.pending_crash_details.lock().take();
|
||||
send_crash_report(crash_details, result.ok());
|
||||
|
||||
minidumper::LoopAction::Exit
|
||||
}
|
||||
|
||||
fn on_client_disconnected(&self, num_clients: usize) -> minidumper::LoopAction {
|
||||
if num_clients == 0 {
|
||||
log::info!("All clients disconnected, shutting down minidump server");
|
||||
minidumper::LoopAction::Exit
|
||||
} else {
|
||||
minidumper::LoopAction::Continue
|
||||
}
|
||||
}
|
||||
|
||||
fn on_message(&self, _kind: u32, buffer: Vec<u8>) {
|
||||
match bincode::deserialize::<MinidumpCommand>(&buffer) {
|
||||
Ok(MinidumpCommand::Shutdown) => {
|
||||
self.shutdown.store(true, Ordering::Relaxed);
|
||||
}
|
||||
Ok(MinidumpCommand::SetTags { tags }) => {
|
||||
sentry::configure_scope(|scope| {
|
||||
for (key, value) in tags {
|
||||
scope.set_tag(&key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(MinidumpCommand::SetUser { user_id }) => {
|
||||
sentry::configure_scope(|scope| {
|
||||
scope.set_user(Some(sentry::User {
|
||||
id: Some(user_id),
|
||||
..Default::default()
|
||||
}));
|
||||
});
|
||||
}
|
||||
Ok(MinidumpCommand::AddBreadcrumb { breadcrumb }) => {
|
||||
sentry::add_breadcrumb(breadcrumb);
|
||||
}
|
||||
Ok(MinidumpCommand::SetCrashDetails { details }) => {
|
||||
*self.pending_crash_details.lock() = Some(details);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Unable to deserialize minidump command: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let shutdown = Arc::new(AtomicBool::new(false));
|
||||
let handler = Box::new(Handler {
|
||||
shutdown: shutdown.clone(),
|
||||
pending_crash_details: Default::default(),
|
||||
});
|
||||
|
||||
log::info!(
|
||||
"Starting minidump server listening on {}",
|
||||
socket_path.display()
|
||||
);
|
||||
let result = minidumper::Server::with_name(socket_path)
|
||||
.context("Unable to create minidump server")?
|
||||
.run(handler, &shutdown, Some(2 * PING_INTERVAL))
|
||||
.context("Error running minidump server");
|
||||
if let Err(ref err) = result {
|
||||
log::error!("Error running minidump server: {err:#}");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Uploads a crash report to Sentry, using the current scope.
|
||||
fn send_crash_report(details: Option<String>, dump: Option<minidumper::MinidumpBinary>) {
|
||||
let message = details.as_deref().unwrap_or("Fatal exception");
|
||||
|
||||
let crash_attachment = dump.and_then(|mut dump| {
|
||||
// In most cases, the minidump contents are available in memory. If not, we can read them off disk.
|
||||
let buffer = match dump.contents {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
dump.file.flush().ok()?;
|
||||
dump.file.rewind().ok()?;
|
||||
let mut buffer = Vec::new();
|
||||
dump.file.read_to_end(&mut buffer).ok()?;
|
||||
buffer
|
||||
}
|
||||
};
|
||||
|
||||
Some(Attachment {
|
||||
buffer,
|
||||
filename: "warp-minidump.dmp".to_string(),
|
||||
ty: Some(AttachmentType::Minidump),
|
||||
..Default::default()
|
||||
})
|
||||
});
|
||||
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
// Do not use the crash reporting server for process info.
|
||||
scope.remove_extra("event.process");
|
||||
if let Some(attachment) = crash_attachment {
|
||||
scope.add_attachment(attachment);
|
||||
}
|
||||
},
|
||||
|| sentry::capture_message(message, Level::Error),
|
||||
);
|
||||
}
|
||||
|
||||
impl MinidumpGuard {
|
||||
// NOTE: We CANNOT use `report_error`, `report_if_error`, `log`, or similar here. Those
|
||||
// all send information to Sentry, which can deadlock.
|
||||
|
||||
/// Set up minidump-backed crash reporting. This spawns a child process that reports crashes to
|
||||
/// Sentry, and a crash handler which sends crashes to that child process.
|
||||
pub fn start() -> anyhow::Result<Self> {
|
||||
let socket_name = format!("wcr-{}.sock", Uuid::new_v4().simple());
|
||||
let socket_path = if cfg!(target_os = "macos") {
|
||||
// On macOS, the maximum length of a socket path is fairly short, so use the temp directory.
|
||||
std::env::temp_dir().join(socket_name)
|
||||
} else {
|
||||
galaxy_core::paths::state_dir().join(socket_name)
|
||||
};
|
||||
|
||||
let child =
|
||||
Command::new(std::env::current_exe().context("Unable to get current executable path")?)
|
||||
.arg("minidump-server")
|
||||
.arg(&socket_path)
|
||||
.spawn()
|
||||
.context("Unable to spawn minidump server process")?;
|
||||
|
||||
let client = Arc::new(
|
||||
wait_for_server(socket_path.as_path()).context("Unable to create minidump client")?,
|
||||
);
|
||||
spawn_keepalive_thread(client.clone());
|
||||
|
||||
let client2 = client.clone();
|
||||
|
||||
let crash_handler = CrashHandler::attach(unsafe {
|
||||
crash_handler::make_crash_event(move |crash_context: &CrashContext| {
|
||||
if let Some(details) = format_crash_details(crash_context) {
|
||||
let _ = send_command(
|
||||
client.as_ref(),
|
||||
MinidumpCommand::SetCrashDetails { details },
|
||||
);
|
||||
}
|
||||
|
||||
// Send a ping to the minidump server, ensuring that any messages sent before the
|
||||
// crash event are flushed and processed. This mostly only matters on macOS.
|
||||
let _ = client.ping();
|
||||
|
||||
let dump_result = client.request_dump(crash_context);
|
||||
crash_handler::CrashEventResult::Handled(dump_result.is_ok())
|
||||
})
|
||||
})
|
||||
.context("Failed to attach crash signal handler")?;
|
||||
|
||||
// Ensure that the crash server process can ptrace Warp.
|
||||
#[cfg(target_os = "linux")]
|
||||
crash_handler.set_ptracer(Some(child.id()));
|
||||
|
||||
let guard = MinidumpGuard {
|
||||
child,
|
||||
client: client2,
|
||||
crash_handler,
|
||||
};
|
||||
|
||||
// Forward any existing tags to the minidump server.
|
||||
guard.set_tags(super::TAGS.read().clone());
|
||||
|
||||
Ok(guard)
|
||||
}
|
||||
|
||||
/// Send the user id for the minidump server to attach to Sentry events.
|
||||
fn set_user_id(&self, user_id: String) {
|
||||
let _ = send_command(self.client.as_ref(), MinidumpCommand::SetUser { user_id });
|
||||
}
|
||||
|
||||
/// Send tags for the minidump server to attach to Sentry events.
|
||||
fn set_tags(&self, tags: HashMap<String, String>) {
|
||||
let _ = send_command(self.client.as_ref(), MinidumpCommand::SetTags { tags });
|
||||
}
|
||||
|
||||
/// Add a breadcrumb to crash reports produced by the minidump server.
|
||||
fn add_breadcrumb(&self, breadcrumb: Breadcrumb) {
|
||||
let _ = send_command(
|
||||
self.client.as_ref(),
|
||||
MinidumpCommand::AddBreadcrumb { breadcrumb },
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulate a crash.
|
||||
pub fn crash(&self) {
|
||||
#[cfg(target_os = "linux")]
|
||||
self.crash_handler.simulate_signal(libc::SIGSEGV as _);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
self.crash_handler.simulate_exception(None);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MinidumpGuard {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the crash handler will detach it.
|
||||
// We can report errors here, as the minidump handler is no longer active.
|
||||
|
||||
// Send a graceful shutdown command before killing the child process.
|
||||
if let Err(err) = send_command(&self.client, MinidumpCommand::Shutdown) {
|
||||
log::warn!("Unable to send shutdown command to minidump child process: {err:#}");
|
||||
}
|
||||
|
||||
if let Err(err) = self.child.kill() {
|
||||
log::warn!("Unable to kill minidump child process: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the minidump server to start and return a client handle.
|
||||
///
|
||||
/// Creating a [`minidumper::Client`] will fail unless the server has started.
|
||||
fn wait_for_server(socket_path: &Path) -> anyhow::Result<minidumper::Client> {
|
||||
let start = instant::Instant::now();
|
||||
|
||||
let mut last_error = None;
|
||||
while start.elapsed() < Duration::from_secs(1) {
|
||||
match minidumper::Client::with_name(socket_path) {
|
||||
Ok(client) => {
|
||||
return Ok(client);
|
||||
}
|
||||
Err(err) => {
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match last_error {
|
||||
Some(err) => Err(err.into()),
|
||||
None => Err(anyhow::anyhow!("Unable to connect to minidump server")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a thread that periodically pings the minidump server to prevent it from idling out.
|
||||
fn spawn_keepalive_thread(client: Arc<minidumper::Client>) {
|
||||
let _ = std::thread::Builder::new()
|
||||
.name("minidump-keepalive".to_string())
|
||||
.spawn(move || loop {
|
||||
// Assume that if a ping fails, the server was shut down - the only purpose of this thread
|
||||
// is to prevent an idle timeout.
|
||||
if client.ping().is_err() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(PING_INTERVAL);
|
||||
});
|
||||
}
|
||||
|
||||
/// Use `client` to send a command to the minidump server.
|
||||
fn send_command(client: &minidumper::Client, command: MinidumpCommand) -> anyhow::Result<()> {
|
||||
let message = bincode::serialize(&command).context("Failed to serialize minidump command")?;
|
||||
client
|
||||
.send_message(0, message)
|
||||
.context("Failed to send minidump command")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum MinidumpCommand {
|
||||
Shutdown,
|
||||
SetTags { tags: HashMap<String, String> },
|
||||
SetUser { user_id: String },
|
||||
AddBreadcrumb { breadcrumb: Breadcrumb },
|
||||
SetCrashDetails { details: String },
|
||||
}
|
||||
|
||||
/// Format details from a [`CrashContext`] into a Sentry error message. This information should
|
||||
/// already be in the minidump, but it's useful to surface prominently in Sentry.
|
||||
fn format_crash_details(crash_context: &CrashContext) -> Option<String> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
Some(format!("Killed by signal {} / {}", crash_context.siginfo.ssi_signo, crash_context.siginfo.ssi_code))
|
||||
} else if #[cfg(target_os = "windows")] {
|
||||
Some(format!("Exception {}", crash_context.exception_code))
|
||||
} else if #[cfg(target_os = "macos")] {
|
||||
crash_context.exception.as_ref().map(|exception| {
|
||||
format!("Exception {} ({} / {:?})", exception.kind, exception.code, exception.subcode)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,11 +337,6 @@ pub trait Experiment<T: Experiment<T>>: FromStr {
|
||||
if let Some(group) = assigned_group.as_ref() {
|
||||
GROUP_ASSIGNMENTS.insert(Self::name(), group.variant());
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
{
|
||||
let tag_name = format!("warp.experiments.{}", Self::name());
|
||||
crate::crash_reporting::set_tag(&tag_name, group.variant());
|
||||
}
|
||||
}
|
||||
|
||||
assigned_group
|
||||
|
||||
@@ -27,12 +27,6 @@ fn enabled_features() -> HashSet<FeatureFlag> {
|
||||
FeatureFlag::Autoupdate,
|
||||
#[cfg(feature = "changelog")]
|
||||
FeatureFlag::Changelog,
|
||||
#[cfg(feature = "cocoa_sentry")]
|
||||
FeatureFlag::CocoaSentry,
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
FeatureFlag::CrashReporting,
|
||||
#[cfg(feature = "log_expensive_frames_in_sentry")]
|
||||
FeatureFlag::LogExpensiveFramesInSentry,
|
||||
#[cfg(feature = "record_app_active_events")]
|
||||
FeatureFlag::RecordAppActiveEvents,
|
||||
#[cfg(feature = "runtime_feature_flags")]
|
||||
@@ -501,8 +495,6 @@ fn enabled_features() -> HashSet<FeatureFlag> {
|
||||
FeatureFlag::GitCredentialRefresh,
|
||||
#[cfg(feature = "remote_code_review")]
|
||||
FeatureFlag::RemoteCodeReview,
|
||||
#[cfg(feature = "custom_inference_endpoints")]
|
||||
FeatureFlag::CustomInferenceEndpoints,
|
||||
#[cfg(feature = "custom_model_routers")]
|
||||
FeatureFlag::CustomModelRouters,
|
||||
#[cfg(feature = "supergrok")]
|
||||
|
||||
+6
-89
@@ -49,8 +49,6 @@ mod completer;
|
||||
mod context_chips;
|
||||
#[cfg(enable_crash_recovery)]
|
||||
mod crash_recovery;
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
mod crash_reporting;
|
||||
mod debug_dump;
|
||||
mod default_terminal;
|
||||
mod download_method;
|
||||
@@ -191,7 +189,6 @@ use repo_metadata::{
|
||||
repositories::DetectedRepositories, watcher::DirectoryWatcher, RepoMetadataModel,
|
||||
};
|
||||
use server::network_log_pane_manager::NetworkLogPaneManager;
|
||||
use server::telemetry::context_provider::AppTelemetryContextProvider;
|
||||
use server::voice_transcriber::ServerVoiceTranscriber;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use settings::import::model::ImportedConfigModel;
|
||||
@@ -320,9 +317,9 @@ use crate::server::cloud_objects::update_manager::UpdateManager;
|
||||
use crate::server::experiments::ServerExperiments;
|
||||
use crate::server::sync_queue::{QueueItem, SyncQueue};
|
||||
pub use crate::server::telemetry::{
|
||||
AgentModeEntrypoint, AgentModeEntrypointSelectionType, TelemetryEvent,
|
||||
AgentModeEntrypoint, AgentModeEntrypointSelectionType, AppStartupInfo, CloseTarget,
|
||||
PaletteSource, TelemetryEvent,
|
||||
};
|
||||
use crate::server::telemetry::{AppStartupInfo, CloseTarget, PaletteSource, TelemetryCollector};
|
||||
use crate::session_management::{RunningSessionSummary, SessionNavigationData};
|
||||
use crate::settings::cloud_preferences_syncer::initialize_cloud_preferences_syncer;
|
||||
use crate::settings::manager::SettingsManager;
|
||||
@@ -566,19 +563,6 @@ impl LaunchMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether Sentry / crash reporting should be initialized.
|
||||
#[cfg_attr(not(feature = "crash_reporting"), allow(dead_code))]
|
||||
pub(crate) fn needs_crash_reporting(&self) -> bool {
|
||||
match self {
|
||||
LaunchMode::App { .. }
|
||||
| LaunchMode::CommandLine { .. }
|
||||
| LaunchMode::Test { .. }
|
||||
| LaunchMode::RemoteServerDaemon { .. }
|
||||
| LaunchMode::RemoteServerProxy
|
||||
| LaunchMode::Tui { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether profiling and tracing should be initialized.
|
||||
pub(crate) fn needs_profiling(&self) -> bool {
|
||||
match self {
|
||||
@@ -781,14 +765,8 @@ fn run_worker_command(worker: &warp_cli::WorkerCommand) -> Result<()> {
|
||||
warp_cli::WorkerCommand::PluginHost { .. } => crate::run_plugin_host(),
|
||||
#[cfg(feature = "local_tty")]
|
||||
warp_cli::WorkerCommand::MinidumpServer { socket_name } => {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(all(linux_or_windows, feature = "crash_reporting"))] {
|
||||
crate::crash_reporting::run_minidump_server(socket_name)
|
||||
} else {
|
||||
let _ = socket_name;
|
||||
panic!("The minidump server is not supported on this platform");
|
||||
}
|
||||
}
|
||||
let _ = socket_name;
|
||||
panic!("The minidump server is not supported");
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
warp_cli::WorkerCommand::RemoteServerProxy(args) => {
|
||||
@@ -912,15 +890,6 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
// for other entrypoints.
|
||||
features::init_feature_flags();
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
if launch_mode.needs_crash_reporting() {
|
||||
// Ensure that the main/root Sentry hub is initialized on the main
|
||||
// thread. PtySpawner creates a background thread to receive logs from
|
||||
// the terminal server process, and we don't want it to be the host of
|
||||
// the primary sentry::Hub.
|
||||
sentry::Hub::main();
|
||||
}
|
||||
|
||||
let mut tracing_initialization = launch_mode
|
||||
.needs_profiling()
|
||||
.then(tracing::init)
|
||||
@@ -982,17 +951,6 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
web_intent_parser::set_context_flags_from_current_url();
|
||||
}
|
||||
|
||||
// Collect errors that occur in run_internal() before the Sentry client is initialized,
|
||||
// so they can be replayed to Sentry once it's ready.
|
||||
#[cfg_attr(
|
||||
not(all(
|
||||
feature = "release_bundle",
|
||||
any(windows, any(target_os = "linux", target_os = "freebsd"))
|
||||
)),
|
||||
expect(unused_mut)
|
||||
)]
|
||||
let mut pre_sentry_errors: Vec<anyhow::Error> = Vec::new();
|
||||
|
||||
#[cfg(all(
|
||||
feature = "release_bundle",
|
||||
any(target_os = "linux", target_os = "freebsd")
|
||||
@@ -1014,7 +972,6 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
Err(err) => {
|
||||
let err = anyhow::Error::from(err).context("Failed to forward startup args");
|
||||
log::error!("{err:#}");
|
||||
pre_sentry_errors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +994,6 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
Err(err) => {
|
||||
let err = anyhow::Error::from(err).context("Failed to forward startup args");
|
||||
log::error!("{err:#}");
|
||||
pre_sentry_errors.push(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1192,9 +1148,6 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::set_client_type_tag(launch_mode.execution_mode().client_id());
|
||||
|
||||
// Add the terminal server singleton to the application.
|
||||
#[cfg(feature = "local_tty")]
|
||||
ctx.add_singleton_model(move |_ctx| pty_spawner);
|
||||
@@ -1218,7 +1171,7 @@ fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
||||
timer,
|
||||
startup_toml_parse_error,
|
||||
ctx,
|
||||
pre_sentry_errors,
|
||||
std::iter::empty(),
|
||||
);
|
||||
|
||||
if ImprovedPaletteSearch::improved_search_enabled(ctx) {
|
||||
@@ -1253,9 +1206,6 @@ pub(crate) fn initialize_app(
|
||||
ctx: &mut galaxyui::AppContext,
|
||||
_pre_sentry_errors: impl IntoIterator<Item = anyhow::Error>,
|
||||
) -> Option<AppState> {
|
||||
// WARNING: Errors that happen here before crash_reporting::init will not be collected in
|
||||
// Sentry. Only the dependencies of crash_reporting should be initialized here. Avoid adding
|
||||
// any other stuff here, as failures will be silent. Push them to pre_sentry_errors instead.
|
||||
let data_domain = ChannelState::data_domain();
|
||||
|
||||
// Daemon auth arrives through the client handshake, so avoid platform keychains that may
|
||||
@@ -1370,8 +1320,6 @@ pub(crate) fn initialize_app(
|
||||
|
||||
ctx.add_singleton_model(|_ctx| AuthStateProvider::new(auth_state.clone()));
|
||||
|
||||
ctx.add_singleton_model(AppTelemetryContextProvider::new_context_provider);
|
||||
|
||||
ctx.add_singleton_model(|ctx| {
|
||||
AuthManager::new(
|
||||
server_api.clone(),
|
||||
@@ -1572,20 +1520,6 @@ pub(crate) fn initialize_app(
|
||||
|
||||
ctx.add_singleton_model(AntivirusInfo::new);
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "crash_reporting")] {
|
||||
let is_crash_reporting_enabled = crash_reporting::init(ctx);
|
||||
} else {
|
||||
let is_crash_reporting_enabled = false;
|
||||
}
|
||||
}
|
||||
// Send buffered pre-init errors to Sentry now that the client is ready.
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
for err in _pre_sentry_errors {
|
||||
sentry::integrations::anyhow::capture_anyhow(&err);
|
||||
}
|
||||
timer.mark_interval_end("INIT_CRASH_REPORTING");
|
||||
|
||||
if let LaunchMode::App { .. } = launch_mode {
|
||||
autoupdate::check_and_report_update_errors(ctx);
|
||||
}
|
||||
@@ -1708,7 +1642,7 @@ pub(crate) fn initialize_app(
|
||||
is_session_restoration_on: user_defaults_on_startup.should_restore_session,
|
||||
is_screen_reader_enabled,
|
||||
from_relaunch,
|
||||
is_crash_reporting_enabled,
|
||||
is_crash_reporting_enabled: false,
|
||||
timing_data,
|
||||
});
|
||||
|
||||
@@ -1834,15 +1768,6 @@ pub(crate) fn initialize_app(
|
||||
|
||||
ctx.add_singleton_model(CustomSecretRegexUpdater::new);
|
||||
|
||||
// Register the `TelemetryCollection` singleton model.
|
||||
let server_api_clone = server_api.clone();
|
||||
ctx.add_singleton_model(|ctx| {
|
||||
let telemetry_collector = TelemetryCollector::new(server_api_clone);
|
||||
telemetry_collector.initialize_telemetry_collection(ctx);
|
||||
telemetry_collector
|
||||
});
|
||||
timer.mark_interval_end("INITIALIZE_TELEMETRY_COLLECTION");
|
||||
|
||||
// Register initial keybindings prior to creating menus
|
||||
ai::init(ctx);
|
||||
app_services::init(ctx);
|
||||
@@ -2433,10 +2358,6 @@ pub(crate) fn app_callbacks(
|
||||
auth_state.user_id().map(|uid| uid.as_string()),
|
||||
auth_state.anonymous_id(),
|
||||
);
|
||||
TelemetryCollector::handle(ctx).update(ctx, |telemetry_collector, ctx| {
|
||||
telemetry_collector.flush_telemetry_events_for_shutdown(ctx);
|
||||
});
|
||||
|
||||
// Shutdown all LSP servers gracefully before app termination
|
||||
lsp::LspManagerModel::handle(ctx).update(ctx, |manager, ctx| {
|
||||
manager.terminate(ctx);
|
||||
@@ -2473,10 +2394,6 @@ pub(crate) fn app_callbacks(
|
||||
initialization.shutdown();
|
||||
}
|
||||
|
||||
// Tear down crash reporting as the last thing we do before the application
|
||||
// terminates.
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crash_reporting::uninit_sentry();
|
||||
})),
|
||||
on_should_close_window: Some(Box::new(move |window_id, ctx| {
|
||||
let general_settings = GeneralSettings::as_ref(ctx);
|
||||
|
||||
@@ -246,39 +246,8 @@ unsafe fn init_logging() {
|
||||
// valid C string pointer.
|
||||
let msg = unsafe { CStr::from_ptr(msg) };
|
||||
let err_message = String::from_utf8_lossy(msg.to_bytes());
|
||||
// Sentry shouldn't panic, but to be safe, make sure we don't unwind across the FFI
|
||||
// boundary.
|
||||
// Do not unwind across the FFI boundary.
|
||||
let _ = panic::catch_unwind(|| {
|
||||
// We report SQLite errors to Sentry in a more-structured format so that they have
|
||||
// better grouping (all are under the same Sentry issue, with details for the specific
|
||||
// error kind). Warning and debug SQLite messages are logged - with the default
|
||||
// sentry_log configuration, warnings are added as breadcrumbs to other events and
|
||||
// debug messages are ignored.
|
||||
// In local builds without crash reporting, all SQLite messages get logged locally.
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
if level == log::Level::Error {
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
let mut context = std::collections::BTreeMap::new();
|
||||
context.insert("message".to_string(), err_message.into());
|
||||
context.insert("code".to_string(), err_code.into());
|
||||
context.insert(
|
||||
"code_description".to_string(),
|
||||
sqlite3::code_to_str(err_code).into(),
|
||||
);
|
||||
scope.set_context("sqlite", sentry::protocol::Context::Other(context));
|
||||
},
|
||||
|| {
|
||||
sentry::capture_message(
|
||||
"Sqlite Error",
|
||||
sentry_log::convert_log_level(level),
|
||||
)
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log::log!(
|
||||
level,
|
||||
"SQLite error {} ({}): {}",
|
||||
|
||||
@@ -25,17 +25,7 @@ impl ipc::ServiceImpl for LogServiceImpl {
|
||||
let log_fn = || {
|
||||
log::log!(target: target.as_str(), level, "{message}");
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "crash_reporting")] {
|
||||
// Explicitly write the log line in the context of the main
|
||||
// Sentry hub; this log receiver thread is spawned before
|
||||
// Sentry is configured, so the thread-local hub doesn't
|
||||
// have the appropriate client and scope configuration.
|
||||
sentry::Hub::run(sentry::Hub::main(), log_fn);
|
||||
} else {
|
||||
log_fn();
|
||||
}
|
||||
}
|
||||
log_fn();
|
||||
LogServiceResponse { success: true }
|
||||
}
|
||||
}
|
||||
|
||||
+6
-37
@@ -61,53 +61,22 @@ pub fn dump_dhat_heap_profile() {
|
||||
let _ = HEAP_PROFILER.lock().take();
|
||||
}
|
||||
|
||||
/// Dumps a jemalloc heap profile and sends it to Sentry.
|
||||
/// Dumps a jemalloc heap profile for local diagnostics.
|
||||
///
|
||||
/// On Linux the profile is produced in-process via the `jemalloc_pprof` crate
|
||||
/// as a raw (unsymbolized) pprof -- sample addresses + mappings + GNU build-id
|
||||
/// -- and is symbolized offline against the debug-info file uploaded to Sentry
|
||||
/// by the release process (matched by build-id). On other platforms it spawns
|
||||
/// -- and is symbolized offline. On other platforms it spawns
|
||||
/// the bundled `pprof` binary to fetch and symbolicate the heap profile from
|
||||
/// the local HTTP server. Either way, the resulting profile is attached to a
|
||||
/// Sentry event.
|
||||
/// the local HTTP server. Either way, the resulting profile is logged locally.
|
||||
#[cfg(feature = "heap_usage_tracking")]
|
||||
pub async fn dump_jemalloc_heap_profile(memory_breakdown: serde_json::Value) {
|
||||
use sentry::protocol::{Attachment, AttachmentType};
|
||||
|
||||
let result = dump_jemalloc_heap_profile_inner().await;
|
||||
match result {
|
||||
Ok(profile_data) => {
|
||||
let attachment = Attachment {
|
||||
buffer: profile_data,
|
||||
filename: "heap-profile.pb".to_string(),
|
||||
ty: Some(AttachmentType::Attachment),
|
||||
..Default::default()
|
||||
};
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
scope.add_attachment(attachment);
|
||||
|
||||
// Attach the memory breakdown as structured context so it
|
||||
// is visible directly in the Sentry event.
|
||||
if let serde_json::Value::Object(map) = memory_breakdown {
|
||||
let context_map: std::collections::BTreeMap<
|
||||
String,
|
||||
sentry::protocol::Value,
|
||||
> = map.into_iter().collect();
|
||||
scope.set_context(
|
||||
"memory_breakdown",
|
||||
sentry::protocol::Context::Other(context_map),
|
||||
);
|
||||
}
|
||||
},
|
||||
|| {
|
||||
sentry::capture_message(
|
||||
"Excessive memory usage detected",
|
||||
sentry::Level::Warning,
|
||||
)
|
||||
},
|
||||
log::warn!(
|
||||
"Excessive memory usage detected; heap profile generated ({} bytes): {memory_breakdown}",
|
||||
profile_data.len()
|
||||
);
|
||||
log::info!("Sent heap profile to Sentry");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("Failed to dump heap profile: {err:#}");
|
||||
|
||||
@@ -656,14 +656,6 @@ pub fn create_transferred_window(
|
||||
new_window_id
|
||||
}
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
fn on_gpu_driver_selected_callback() -> Option<Box<OnGPUDeviceSelected>> {
|
||||
Some(Box::new(|gpu_device_info| {
|
||||
crate::crash_reporting::set_gpu_device_info(gpu_device_info)
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crash_reporting"))]
|
||||
fn on_gpu_driver_selected_callback() -> Option<Box<OnGPUDeviceSelected>> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -577,9 +577,7 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
)
|
||||
.with_group(bindings::BindingGroup::WarpAi)
|
||||
.is_supported_on_current_platform(
|
||||
UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app)
|
||||
|| (FeatureFlag::CustomInferenceEndpoints.is_enabled()
|
||||
&& UserWorkspaces::as_ref(app).is_custom_inference_enabled(app)),
|
||||
UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app),
|
||||
),
|
||||
ToggleSettingActionPair::new(
|
||||
"auto show or hide Rich Input based on agent status",
|
||||
@@ -7640,8 +7638,6 @@ struct ApiKeysWidget {
|
||||
can_use_warp_credits_for_fallback: SwitchStateHandle,
|
||||
upgrade_highlight_index: HighlightedHyperlink,
|
||||
|
||||
custom_inference_info_tooltip: MouseStateHandle,
|
||||
custom_inference_terms_index: HighlightedHyperlink,
|
||||
description_learn_more_index: HighlightedHyperlink,
|
||||
}
|
||||
|
||||
@@ -7860,8 +7856,6 @@ impl ApiKeysWidget {
|
||||
can_use_warp_credits_for_fallback: Default::default(),
|
||||
upgrade_highlight_index: Default::default(),
|
||||
|
||||
custom_inference_info_tooltip: Default::default(),
|
||||
custom_inference_terms_index: Default::default(),
|
||||
description_learn_more_index: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -7935,175 +7929,6 @@ impl ApiKeysWidget {
|
||||
column.finish()
|
||||
}
|
||||
|
||||
fn render_custom_inference_description(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let text_fragments = vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"Use your own API keys from model providers for Warp Agent. You can also add custom endpoints to use third-party models. Custom endpoints must support the OpenAI-compatible Chat Completions API. API keys are stored only on your device, never on Warp's servers. They're used to make requests to your chosen model provider. Using auto models or models from providers you have not provided API keys for will consume Warp credits. ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink("Learn more", CUSTOM_INFERENCE_LEARN_MORE_URL),
|
||||
];
|
||||
let description = FormattedTextElement::new(
|
||||
FormattedText::new([FormattedTextLine::Line(text_fragments)]),
|
||||
CONTENT_FONT_SIZE,
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_family(),
|
||||
blended_colors::text_sub(appearance.theme(), appearance.theme().surface_1()),
|
||||
self.description_learn_more_index.clone(),
|
||||
)
|
||||
.with_hyperlink_font_color(appearance.theme().accent().into_solid())
|
||||
.register_default_click_handlers(|url, ctx, _| {
|
||||
ctx.dispatch_typed_action(AISettingsPageAction::HyperlinkClick(url));
|
||||
});
|
||||
Container::new(description.finish())
|
||||
.with_margin_top(styles::DESCRIPTION_NEGATIVE_MARGIN_OFFSET)
|
||||
.with_margin_bottom(styles::DESCRIPTION_MARGIN_BOTTOM)
|
||||
.with_margin_right(styles::TOGGLE_WIDTH_MARGIN)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_custom_inference_info_icon(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let icon = Container::new(
|
||||
ConstrainedBox::new(
|
||||
Icon::Info
|
||||
.to_galaxyui_icon(appearance.theme().active_ui_text_color())
|
||||
.finish(),
|
||||
)
|
||||
.with_width(13.)
|
||||
.with_height(13.)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let tooltip_text = FormattedText::new([FormattedTextLine::Line(vec![
|
||||
FormattedTextFragment::plain_text(
|
||||
"By using BYOK or custom endpoints, you agree to use them only as permitted by ",
|
||||
),
|
||||
FormattedTextFragment::hyperlink("Warp's Terms of Service", CUSTOM_INFERENCE_TERMS_URL),
|
||||
FormattedTextFragment::plain_text(
|
||||
". BYOK and custom endpoints are intended for individual use and small teams. Companies or organizations with more than 10 employees should use Warp Business or Enterprise.",
|
||||
),
|
||||
])]);
|
||||
let tooltip_background = appearance.theme().tooltip_background();
|
||||
|
||||
let info_button =
|
||||
Hoverable::new(self.custom_inference_info_tooltip.clone(), move |state| {
|
||||
let mut stack = Stack::new().with_child(icon);
|
||||
if state.is_hovered() {
|
||||
let tool_tip = ConstrainedBox::new(
|
||||
Container::new(
|
||||
FormattedTextElement::new(
|
||||
tooltip_text.clone(),
|
||||
10.,
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_family(),
|
||||
appearance.theme().background().into_solid(),
|
||||
self.custom_inference_terms_index.clone(),
|
||||
)
|
||||
.with_hyperlink_font_color(
|
||||
appearance
|
||||
.theme()
|
||||
.accent()
|
||||
.on_background(
|
||||
ThemeFill::Solid(tooltip_background),
|
||||
MinimumAllowedContrast::Text,
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.register_default_click_handlers(|url, ctx, _| {
|
||||
ctx.dispatch_typed_action(AISettingsPageAction::HyperlinkClick(
|
||||
url,
|
||||
));
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.with_background_color(tooltip_background)
|
||||
.with_vertical_padding(4.)
|
||||
.with_horizontal_padding(8.)
|
||||
.with_border(Border::all(1.).with_border_fill(appearance.theme().outline()))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.finish(),
|
||||
)
|
||||
.with_max_width(CUSTOM_INFERENCE_INFO_TOOLTIP_MAX_WIDTH)
|
||||
.finish();
|
||||
stack.add_positioned_child(
|
||||
tool_tip,
|
||||
OffsetPositioning::offset_from_parent(
|
||||
vec2f(0., -3.),
|
||||
ParentOffsetBounds::WindowByPosition,
|
||||
ParentAnchor::TopMiddle,
|
||||
ChildAnchor::BottomMiddle,
|
||||
),
|
||||
);
|
||||
}
|
||||
stack.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand);
|
||||
|
||||
Container::new(Box::new(info_button))
|
||||
.with_margin_left(4.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_custom_endpoints_list(
|
||||
&self,
|
||||
view: &AISettingsPageView,
|
||||
appearance: &Appearance,
|
||||
is_enabled: bool,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let text_color = styles::header_font_color(is_enabled, app);
|
||||
let endpoints = &ApiKeyManager::as_ref(app).keys().custom_endpoints;
|
||||
let chip_border = internal_colors::fg_overlay_3(theme);
|
||||
|
||||
let mut list = Flex::column().with_spacing(12.);
|
||||
for (index, endpoint) in endpoints.iter().enumerate() {
|
||||
let model_labels = endpoint
|
||||
.models
|
||||
.iter()
|
||||
.map(|model| model.alias.clone().unwrap_or_else(|| model.name.clone()))
|
||||
.filter(|s| !s.trim().is_empty());
|
||||
|
||||
let chips = super::render_model_chips(model_labels, appearance, text_color);
|
||||
|
||||
let endpoint_name = Text::new_inline(
|
||||
endpoint.name.clone(),
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_size(),
|
||||
)
|
||||
.with_style(Properties::default().weight(Weight::Semibold))
|
||||
.with_color(text_color.into())
|
||||
.finish();
|
||||
|
||||
let left = Flex::column()
|
||||
.with_spacing(8.)
|
||||
.with_child(endpoint_name)
|
||||
.with_child(chips)
|
||||
.finish();
|
||||
|
||||
let edit_button = Empty::new().finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(Shrinkable::new(1., left).finish())
|
||||
.with_child(edit_button)
|
||||
.finish();
|
||||
|
||||
list.add_child(
|
||||
Container::new(row)
|
||||
.with_uniform_padding(12.)
|
||||
.with_background(internal_colors::fg_overlay_1(theme))
|
||||
.with_border(Border::all(1.).with_border_fill(chip_border))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(6.)))
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
list.finish()
|
||||
}
|
||||
|
||||
/// The "Connect SuperGrok subscription" row: label and description on the
|
||||
/// left, a Connect/Disconnect button on the right, and a "Connected on
|
||||
/// ..." status line underneath while a subscription is connected.
|
||||
@@ -8299,95 +8124,23 @@ impl SettingsWidget for ApiKeysWidget {
|
||||
let ai_settings = AISettings::as_ref(app);
|
||||
let is_any_ai_enabled = ai_settings.is_any_ai_enabled(app);
|
||||
let is_byo_enabled = UserWorkspaces::as_ref(app).is_byo_api_key_enabled(app);
|
||||
let is_custom_inference_enabled =
|
||||
UserWorkspaces::as_ref(app).is_custom_inference_enabled(app);
|
||||
let provider_keys_enabled = is_any_ai_enabled && is_byo_enabled;
|
||||
let custom_inference_controls_enabled = is_any_ai_enabled && is_custom_inference_enabled;
|
||||
let custom_inference_flag_on = FeatureFlag::CustomInferenceEndpoints.is_enabled();
|
||||
let show_custom_inference = custom_inference_flag_on && is_custom_inference_enabled;
|
||||
|
||||
let mut column = Flex::column().with_child(render_separator(appearance));
|
||||
|
||||
if show_custom_inference {
|
||||
// Header row: "Custom inference" + info icon on left, "+ Add custom model" on right
|
||||
let header_left = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
build_sub_header(
|
||||
appearance,
|
||||
"Custom inference",
|
||||
Some(styles::header_font_color(
|
||||
custom_inference_controls_enabled,
|
||||
app,
|
||||
)),
|
||||
)
|
||||
.with_margin_bottom(0.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(self.render_custom_inference_info_icon(appearance))
|
||||
.finish();
|
||||
|
||||
let header_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(header_left)
|
||||
.finish();
|
||||
|
||||
column.add_child(
|
||||
Container::new(header_row)
|
||||
.with_padding_bottom(HEADER_PADDING)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// Description with Learn more link
|
||||
column.add_child(self.render_custom_inference_description(app));
|
||||
} else {
|
||||
// Fallback: old "API Keys" header only
|
||||
column.add_child(
|
||||
build_sub_header(
|
||||
appearance,
|
||||
"API Keys",
|
||||
Some(styles::header_font_color(is_any_ai_enabled, app)),
|
||||
)
|
||||
.with_padding_bottom(HEADER_PADDING)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
column.add_child(
|
||||
build_sub_header(
|
||||
appearance,
|
||||
"API Keys",
|
||||
Some(styles::header_font_color(is_any_ai_enabled, app)),
|
||||
)
|
||||
.with_padding_bottom(HEADER_PADDING)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
// Provider key editors (always visible)
|
||||
column.add_child(self.render_provider_key_editors(appearance, provider_keys_enabled, app));
|
||||
|
||||
// Custom endpoints sub-label + list (only when flag on and endpoints non-empty)
|
||||
if show_custom_inference {
|
||||
let endpoints = &ApiKeyManager::as_ref(app).keys().custom_endpoints;
|
||||
if !endpoints.is_empty() {
|
||||
column.add_child(
|
||||
Container::new(
|
||||
Text::new_inline(
|
||||
"Custom endpoints",
|
||||
appearance.ui_font_family(),
|
||||
CONTENT_FONT_SIZE,
|
||||
)
|
||||
.with_color(
|
||||
styles::header_font_color(custom_inference_controls_enabled, app)
|
||||
.into(),
|
||||
)
|
||||
.with_style(Properties::default().weight(Weight::Semibold))
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(16.)
|
||||
.with_margin_bottom(8.)
|
||||
.finish(),
|
||||
);
|
||||
column.add_child(self.render_custom_endpoints_list(
|
||||
view,
|
||||
appearance,
|
||||
custom_inference_controls_enabled,
|
||||
app,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Entrypoint for connecting a SuperGrok (xAI) subscription via OAuth.
|
||||
if FeatureFlag::SuperGrok.is_enabled() {
|
||||
@@ -8420,8 +8173,8 @@ impl SettingsWidget for ApiKeysWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// Warp credit fallback toggle (shown when BYO or custom inference is enabled)
|
||||
if is_byo_enabled || show_custom_inference {
|
||||
// Warp credit fallback toggle (shown when BYO is enabled)
|
||||
if is_byo_enabled {
|
||||
column.add_child(
|
||||
Container::new(self.render_warp_credit_fallback_toggle(view, app))
|
||||
.with_margin_top(16.)
|
||||
|
||||
@@ -454,7 +454,6 @@ pub mod flags {
|
||||
pub const TELEMETRY_FLAG: &str = "telemetry";
|
||||
pub const SETTINGS_SYNC_FLAG: &str = "settings_sync";
|
||||
pub const SAFE_MODE_FLAG: &str = "safe_mode";
|
||||
pub const CRASH_REPORTING_FLAG: &str = "crash_reporting";
|
||||
pub const CLOUD_CONVERSATION_STORAGE_FLAG: &str = "Cloud_Conversation_Storage_Enabled";
|
||||
pub const CLOUD_CONVERSATION_STORAGE_EDITABLE_FLAG: &str =
|
||||
"Cloud_Conversation_Storage_Editable";
|
||||
@@ -1885,11 +1884,6 @@ impl SettingsView {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
{
|
||||
crate::crash_reporting::set_tag("warp.settings_page", section.to_string());
|
||||
}
|
||||
|
||||
if let Some(settings_page) = self.current_settings_page() {
|
||||
update_page!(
|
||||
&settings_page.view_handle,
|
||||
|
||||
@@ -279,17 +279,6 @@ impl PrivacyPageView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn toggle_crash_reporting(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let privacy_settings_handle = PrivacySettings::handle(ctx);
|
||||
let old_value = privacy_settings_handle
|
||||
.as_ref(ctx)
|
||||
.is_crash_reporting_enabled;
|
||||
ctx.update_model(&privacy_settings_handle, |privacy_settings, ctx| {
|
||||
privacy_settings.set_is_crash_reporting_enabled(!old_value, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn queue_regex_removal(&mut self, idx: usize, ctx: &mut ViewContext<Self>) {
|
||||
// Check if this removal is already pending
|
||||
if self.pending_regex_removals.contains(&idx) {
|
||||
@@ -454,7 +443,6 @@ pub enum PrivacyPageAction {
|
||||
ToggleHideSecretsInBlockList,
|
||||
SetSecretDisplayMode(SecretDisplayMode),
|
||||
ToggleTelemetry,
|
||||
ToggleCrashReporting,
|
||||
LaunchNetworkLogging,
|
||||
RemoveCustomRegex(usize),
|
||||
AddAllRecommendedRegexes,
|
||||
@@ -534,7 +522,6 @@ impl TypedActionView for PrivacyPageView {
|
||||
self.set_secret_display_mode(*mode, ctx)
|
||||
}
|
||||
PrivacyPageAction::ToggleTelemetry => self.toggle_telemetry(ctx),
|
||||
PrivacyPageAction::ToggleCrashReporting => self.toggle_crash_reporting(ctx),
|
||||
PrivacyPageAction::ToggleCloudConversationStorage => {
|
||||
let handle = PrivacySettings::handle(ctx);
|
||||
ctx.update_model(&handle, |settings, ctx| {
|
||||
@@ -1539,11 +1526,6 @@ impl SettingsWidget for CrashReportsWidget {
|
||||
}
|
||||
|
||||
fn should_render(&self, app: &AppContext) -> bool {
|
||||
// Builds without a crash reporting config (e.g. OpenWarp) cannot ship
|
||||
// crash reports, so the toggle would be a no-op. Hide it in that case.
|
||||
if !ChannelState::is_crash_reporting_available() {
|
||||
return false;
|
||||
}
|
||||
let privacy_settings = PrivacySettings::as_ref(app);
|
||||
!privacy_settings.is_telemetry_force_enabled()
|
||||
}
|
||||
@@ -1555,25 +1537,7 @@ impl SettingsWidget for CrashReportsWidget {
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let ui_builder = appearance.ui_builder();
|
||||
let privacy_settings = PrivacySettings::as_ref(app);
|
||||
Flex::column()
|
||||
.with_child(render_body_item::<PrivacyPageAction>(
|
||||
"Send crash reports".into(),
|
||||
None,
|
||||
// Crash report state is always synced to cloud, so no need to show local only icon.
|
||||
LocalOnlyIconState::Hidden,
|
||||
ToggleState::Enabled,
|
||||
appearance,
|
||||
ui_builder
|
||||
.switch(self.switch_state.clone())
|
||||
.check(privacy_settings.is_crash_reporting_enabled)
|
||||
.build()
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(PrivacyPageAction::ToggleCrashReporting)
|
||||
})
|
||||
.finish(),
|
||||
None,
|
||||
))
|
||||
.with_child(
|
||||
ui_builder
|
||||
.paragraph(
|
||||
@@ -1692,14 +1656,6 @@ pub fn init_actions_from_parent_view<T: Action + Clone>(
|
||||
context,
|
||||
flags::TELEMETRY_FLAG,
|
||||
),
|
||||
ToggleSettingActionPair::new(
|
||||
"crash reporting",
|
||||
builder(SettingsAction::PrivacyPageToggle(
|
||||
PrivacyPageAction::ToggleCrashReporting,
|
||||
)),
|
||||
context,
|
||||
flags::CRASH_REPORTING_FLAG,
|
||||
),
|
||||
];
|
||||
|
||||
toggle_binding_pairs.push(ToggleSettingActionPair::new(
|
||||
|
||||
@@ -193,9 +193,9 @@ impl SystemInfo {
|
||||
// dump and upload the current heap profiling data.
|
||||
#[cfg(feature = "heap_usage_tracking")]
|
||||
{
|
||||
let breakdown_for_sentry = memory_breakdown.clone();
|
||||
let breakdown_for_profile = memory_breakdown.clone();
|
||||
ctx.spawn(
|
||||
crate::profiling::dump_jemalloc_heap_profile(breakdown_for_sentry),
|
||||
crate::profiling::dump_jemalloc_heap_profile(breakdown_for_profile),
|
||||
|_, _, _| {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,15 +99,5 @@ pub(super) fn handle_write_log_request(level: log::Level, target: String, messag
|
||||
// Write the log line that was forwarded from the terminal server.
|
||||
log::log!(target: target.as_str(), level, "{message}");
|
||||
};
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "crash_reporting")] {
|
||||
// Explicitly write the log line in the context of the main
|
||||
// Sentry hub; this log receiver thread is spawned before
|
||||
// Sentry is configured, so the thread-local hub doesn't
|
||||
// have the appropriate client and scope configuration.
|
||||
sentry::Hub::run(sentry::Hub::main(), log_fn);
|
||||
} else {
|
||||
log_fn();
|
||||
}
|
||||
}
|
||||
log_fn();
|
||||
}
|
||||
|
||||
@@ -76,28 +76,9 @@ impl PtyHandle for DirectPtyHandle {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Invokes the provided callback function without crash reporting enabled.
|
||||
fn invoke_without_crash_reporting<T>(
|
||||
is_crash_reporting_enabled: bool,
|
||||
func: impl FnOnce() -> T,
|
||||
) -> T {
|
||||
// Uninitialize cocoa-sentry before spawning the shell process to avoid passing any custom state
|
||||
// (such as BSD signal handlers and mach exception handlers) into the shell process. This means
|
||||
// we lose all Cocoa crash reports from now until when the session is successfully spawned,
|
||||
// which is not ideal but allows us to fully ensure that we don't improperly leak any Sentry state
|
||||
// into the child processes.
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::uninit_cocoa_sentry();
|
||||
|
||||
let retval = func();
|
||||
|
||||
// Now that the child has spawned--reinitialize cocoa sentry.
|
||||
if is_crash_reporting_enabled {
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::init_cocoa_sentry();
|
||||
}
|
||||
|
||||
retval
|
||||
/// Invokes the provided callback function while spawning a shell process.
|
||||
fn invoke_without_crash_reporting<T>(func: impl FnOnce() -> T) -> T {
|
||||
func()
|
||||
}
|
||||
|
||||
pub(super) struct PtySpawnInfo {
|
||||
@@ -239,7 +220,7 @@ impl PtySpawner {
|
||||
is_crash_reporting_enabled: bool,
|
||||
) -> Result<(PtySpawnResult, Box<dyn PtyHandle>)> {
|
||||
let pty_spawn_info =
|
||||
invoke_without_crash_reporting(is_crash_reporting_enabled, move || {
|
||||
invoke_without_crash_reporting(move || {
|
||||
local_tty::spawn(
|
||||
options,
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -1435,26 +1435,6 @@ impl Session {
|
||||
log::warn!(
|
||||
"Failed to read history using PowerShell commands: {powershell_error:?}"
|
||||
);
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
let mut context = std::collections::BTreeMap::new();
|
||||
context.insert(
|
||||
"powershell_error".to_string(),
|
||||
format!("{powershell_error:?}").into(),
|
||||
);
|
||||
scope.set_context(
|
||||
"powershell_history",
|
||||
sentry::protocol::Context::Other(context),
|
||||
);
|
||||
},
|
||||
|| {
|
||||
sentry::capture_message(
|
||||
"Failed to read history using PowerShell commands",
|
||||
sentry::Level::Error,
|
||||
)
|
||||
},
|
||||
);
|
||||
Ok(contents)
|
||||
}
|
||||
Err(e) => Err(ReadHistoryContentsError::PowerShellAndAsyncFsError {
|
||||
|
||||
@@ -130,11 +130,7 @@ pub fn init(app: &mut AppContext) {
|
||||
]);
|
||||
|
||||
if ChannelState::enable_debug_features() {
|
||||
let crash_description = if cfg!(target_os = "macos") {
|
||||
"Crash the app (for testing sentry-cocoa)"
|
||||
} else {
|
||||
"Crash the app (for testing sentry-native)"
|
||||
};
|
||||
let crash_description = "Crash the app (for testing)";
|
||||
app.register_editable_bindings([
|
||||
EditableBinding::new("workspace:crash", crash_description, WorkspaceAction::Crash)
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
@@ -146,7 +142,7 @@ pub fn init(app: &mut AppContext) {
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
EditableBinding::new(
|
||||
"workspace:panic",
|
||||
"Trigger a panic (for testing sentry-rust)",
|
||||
"Trigger a panic (for testing)",
|
||||
WorkspaceAction::Panic,
|
||||
)
|
||||
.with_context_predicate(id!("Workspace")),
|
||||
|
||||
@@ -31,8 +31,6 @@ use std::convert::TryFrom;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::env;
|
||||
use std::fmt::Write;
|
||||
#[cfg(all(target_os = "macos", feature = "crash_reporting"))]
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::process;
|
||||
@@ -110,8 +108,6 @@ use pathfinder_geometry::rect::RectF;
|
||||
use repo_metadata::repositories::DetectedRepositories;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use repo_metadata::RemoteRepositoryIdentifier;
|
||||
#[cfg(all(target_os = "macos", feature = "crash_reporting"))]
|
||||
use sentry::protocol::{Attachment, AttachmentType};
|
||||
use serde_json;
|
||||
use session_sharing_protocol::common::SessionId as SharedSessionId;
|
||||
#[cfg(target_family = "wasm")]
|
||||
@@ -22623,10 +22619,6 @@ impl Workspace {
|
||||
context.set.insert(flags::CLOUD_CONVERSATION_STORAGE_FLAG);
|
||||
}
|
||||
|
||||
if privacy_settings.is_crash_reporting_enabled {
|
||||
context.set.insert(flags::CRASH_REPORTING_FLAG);
|
||||
}
|
||||
|
||||
if editor_settings.cursor_blink.value() == &CursorBlink::Enabled {
|
||||
context.set.insert(flags::CURSOR_BLINK_CONTEXT_FLAG);
|
||||
}
|
||||
@@ -24479,8 +24471,7 @@ impl TypedActionView for Workspace {
|
||||
self.dismiss_ai_assistant_warm_welcome(ctx);
|
||||
}
|
||||
Crash => {
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
crate::crash_reporting::crash();
|
||||
log::warn!("Crash testing is unavailable in Galaxy");
|
||||
}
|
||||
Panic => {
|
||||
panic!("WorkspaceAction::Panic triggered from command palette");
|
||||
@@ -25349,36 +25340,6 @@ impl TypedActionView for Workspace {
|
||||
Ok(Ok(output)) if output.status.success() => {
|
||||
ctx.open_file_path_in_explorer(Path::new(&output_path));
|
||||
|
||||
#[cfg(feature = "crash_reporting")]
|
||||
if ChannelState::channel().is_dogfood() {
|
||||
// For dogfood process samples, we raise a sentry warning with the sample attatched.
|
||||
// We do this so that our performance bot can then read through the performance logs
|
||||
// in sentry and write up a report of findings/possible optimizations.
|
||||
if let Ok(sample_data) = fs::read(&output_path) {
|
||||
let filename = Path::new(&output_path)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "process_sample.txt".to_string());
|
||||
let attachment = Attachment {
|
||||
buffer: sample_data,
|
||||
filename,
|
||||
ty: Some(AttachmentType::Attachment),
|
||||
..Default::default()
|
||||
};
|
||||
sentry::with_scope(
|
||||
|scope| {
|
||||
scope.add_attachment(attachment);
|
||||
},
|
||||
|| {
|
||||
sentry::capture_message(
|
||||
"[FOR PERFORMANCE BOT] Dev took performance sample with results: ",
|
||||
sentry::Level::Warning,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
format!("Process sample saved to {output_path}")
|
||||
}
|
||||
Ok(Ok(output)) => {
|
||||
|
||||
@@ -491,26 +491,6 @@ impl UserWorkspaces {
|
||||
.map(|workspace| workspace.is_byo_api_key_enabled())
|
||||
.unwrap_or(FeatureFlag::SoloUserByok.is_enabled())
|
||||
}
|
||||
/// Whether custom inference endpoints are enabled for the current user.
|
||||
/// Anonymous or logged-out users are not allowed to use custom inference.
|
||||
/// Enterprise workspaces require the enterprise custom inference flag, Warp Plan, or dogfood.
|
||||
pub fn is_custom_inference_enabled(&self, app: &AppContext) -> bool {
|
||||
if AuthStateProvider::as_ref(app)
|
||||
.get()
|
||||
.is_anonymous_or_logged_out()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current_workspace()
|
||||
.map(|workspace| {
|
||||
workspace.billing_metadata.customer_type != CustomerType::Enterprise
|
||||
|| FeatureFlag::CustomInferenceEndpointsEnterprise.is_enabled()
|
||||
|| ChannelState::channel().is_dogfood()
|
||||
})
|
||||
.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn aws_bedrock_host_settings(&self) -> Option<&super::workspace::LlmHostSettings> {
|
||||
self.current_workspace().and_then(|workspace| {
|
||||
workspace
|
||||
|
||||
Reference in New Issue
Block a user