Fix cursor focus and selection in input box, add AWS env var warning box, and remove AWS Bedrock login banner

This commit is contained in:
2026-07-02 14:54:15 -05:00
parent 4770ac06b5
commit 3769646ca6
1194 changed files with 5312 additions and 8032 deletions
+3 -75
View File
@@ -1,90 +1,18 @@
use std::fs::remove_file;
use std::sync::Arc;
use std::time::Duration;
use galaxyui::{Entity, ModelContext, SingletonEntity};
use super::{clear_event_queue, rudder_event_file_path, RUDDER_TELEMETRY_EVENTS_FILE_NAME};
use crate::auth::AuthStateProvider;
use crate::channel::ChannelState;
use crate::features::FeatureFlag;
use crate::server::server_api::ServerApi;
use crate::settings::{PrivacySettings, PrivacySettingsChangedEvent};
// How often we send Active Usage signals.
const ACTIVE_USAGE_DURATION: Duration = Duration::from_secs(60);
/// Duration to wait before flushing the event queue to Rudderstack.
const TELEMETRY_FLUSH_DURATION: Duration = Duration::from_secs(30);
/// Max telemetry events to write to disk. This is bounded to limit the size of the file as well
/// as latency of writing the file.
const MAX_TELEMETRY_EVENTS_TO_STORE: usize = 20;
/// Maximum time to wait for the telemetry flush network request during shutdown.
/// If the network is unavailable or slow, we don't want the CLI process to hang indefinitely.
const TELEMETRY_SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(5);
/// App singleton responsible for scheduling periodic background tasks for sending batches of
/// telemetry events to Rudderstack. This model respects the user's telemetry enablement setting.
/// No-op telemetry collector. Telemetry has been removed from Galaxy.
pub struct TelemetryCollector {
_server_api: Arc<ServerApi>,
}
impl TelemetryCollector {
pub fn new(server_api: Arc<ServerApi>) -> Self {
Self { server_api }
}
pub fn initialize_telemetry_collection(&self, ctx: &mut ModelContext<TelemetryCollector>) {
// Start a background thread to periodically flush events from the telemetry event queue.
if ChannelState::is_release_bundle() || FeatureFlag::WithSandboxTelemetry.is_enabled() {
// Flush the events to Rudderstack that were persisted into a file the last time the app was
// quit.
self.flush_persisted_events_from_disk(ctx);
}
// Send Active App Usage signals
if FeatureFlag::RecordAppActiveEvents.is_enabled()
&& (ChannelState::is_release_bundle() || FeatureFlag::WithSandboxTelemetry.is_enabled())
{
self.schedule_send_active_usage_event(ctx);
}
// Start a background thread to periodically flush events from the telemetry event queue.
if ChannelState::is_release_bundle()
|| FeatureFlag::WithSandboxTelemetry.is_enabled()
|| FeatureFlag::SendTelemetryToFile.is_enabled()
{
self.schedule_event_queue_flush(ctx);
}
// Clear queued telemetry events when telemetry is enabled or disabled. If telemetry is
// enabled, we will start sending Rudderstack requests when the event queue is periodically
// flushed. The initial request should not contain any events recorded when the user was
// previously opted-out of telemetry. In the case where the user turns the telemetry from
// on to off, we should not send another request with any telemetry, even if the event was
// initially recorded prior to the user turning telemetry off.`
ctx.subscribe_to_model(&PrivacySettings::handle(ctx), |_me, _, event, _ctx| {
if let PrivacySettingsChangedEvent::UpdateIsTelemetryEnabled { .. } = event {
clear_event_queue();
}
});
}
/// Writes all queued but unsent telemetry telemetry events to disk so that they may be sent
/// on the next app startup.
pub fn write_telemetry_events_to_disk(&self, ctx: &mut ModelContext<TelemetryCollector>) {
match self.server_api.persist_telemetry_events(
MAX_TELEMETRY_EVENTS_TO_STORE,
PrivacySettings::as_ref(ctx).get_snapshot(ctx),
) {
Ok(()) => {
log::info!("Successfully wrote telemetry events to disk")
}
Err(e) => {
log::error!("Failed to write telemetry events to disk {e:#}");
}
Self {
_server_api: server_api,
}
}
+5 -3
View File
@@ -1,15 +1,17 @@
#![allow(dead_code)]
//! Module that builds a static context to attach to each of our events that are sent to Rudderstack.
//! This is needed so we know the backing operating system and version of each telemetry event.
use std::sync::OnceLock;
use serde::Serialize;
use serde_json::{json, Value};
use galaxy_core::operating_system_info::OperatingSystemInfo;
#[cfg(target_family = "wasm")]
use galaxyui::platform::wasm;
use serde::Serialize;
use serde_json::{json, Value};
use super::rudder_message::Message as RudderMessage;
use crate::server::OperatingSystemInfo;
static TELEMETRY_CONTEXT: OnceLock<TelemetryContext> = OnceLock::new();
+4 -7
View File
@@ -2,9 +2,10 @@ use std::time::Duration;
use galaxy_completer::completer::MatchType;
use galaxy_core::command::ExitCode;
use galaxy_core::telemetry::EnablementState;
use galaxy_core::telemetry::TelemetryEvent as TelemetryEventTrait;
use galaxy_core::telemetry::TelemetryEventDesc;
use galaxy_core::interval_timer::TimingDataPoint;
use galaxy_core::telemetry::{
EnablementState, TelemetryEvent as TelemetryEventTrait, TelemetryEventDesc,
};
use galaxyui::keymap::Keystroke;
use galaxyui::notification::{NotificationSendError, RequestPermissionsOutcome};
use galaxyui::rendering::ThinStrokes;
@@ -13,10 +14,6 @@ use serde_json::{json, Value};
use session_sharing_protocol::common::{ParticipantId, Role, SessionId as SharedSessionId};
use session_sharing_protocol::sharer::{SessionEndedReason, SessionSourceType};
use strum_macros::{EnumDiscriminants, EnumIter};
use galaxy_core::interval_timer::TimingDataPoint;
use galaxy_core::telemetry::{
EnablementState, TelemetryEvent as TelemetryEventTrait, TelemetryEventDesc,
};
use crate::ai::agent::api::ServerConversationToken;
use crate::ai::agent::conversation::AIConversationId;
+11 -197
View File
@@ -6,47 +6,11 @@ mod macros;
pub mod rudder_message;
pub mod secret_redaction;
use std::fs::File;
#[cfg(not(target_family = "wasm"))]
use std::fs::OpenOptions;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::Result;
use chrono::Utc;
pub use collector::*;
pub use context::telemetry_context;
pub use events::*;
use futures::FutureExt;
use rudder_message::{
Batch as RudderBatch, BatchMessage as RudderBatchMessageWithMetadata,
BatchMessageItem as RudderBatchMessage, Message as RudderMessage,
};
use galaxy_core::channel::RudderStackDestination;
use warpui::telemetry::Event;
use crate::auth::UserUid;
use crate::features::FeatureFlag;
use crate::server::telemetry::context::AttachContext;
use crate::server::telemetry_ext::TelemetryExt;
use crate::settings::PrivacySettingsSnapshot;
use crate::ChannelState;
/// Filename for file where telemetry events are written on app quit.
const RUDDER_TELEMETRY_EVENTS_FILE_NAME: &str = "rudder_telemetry_events.json";
/// Filepath where the Rudder events should be written on app quit.
fn rudder_event_file_path() -> PathBuf {
galaxy_core::paths::secure_state_dir()
.unwrap_or_else(galaxy_core::paths::state_dir)
.join(RUDDER_TELEMETRY_EVENTS_FILE_NAME)
}
/// Removes all telemetry events from the app telemetry event queue.
pub fn clear_event_queue() {
let _ = galaxyui::telemetry::flush_events();
}
/// No-op stub. Telemetry has been removed from Galaxy.
pub struct TelemetryApi {
pub client: http_client::Client,
}
@@ -66,166 +30,11 @@ impl TelemetryApi {
pub async fn send_telemetry_event(
&self,
user_id: Option<UserUid>,
anonymous_id: String,
event: impl galaxy_core::telemetry::TelemetryEvent,
settings_snapshot: PrivacySettingsSnapshot,
) -> Result<()> {
let event = galaxyui::telemetry::create_event(
user_id.map(|uid| uid.as_string()),
anonymous_id,
event.name().into(),
event.payload(),
event.contains_ugc(),
galaxyui::time::get_current_time(),
);
self.send_telemetry_event_internal(event, settings_snapshot)
.await
}
/// Internal implementation for sending telemetry events. This reduces code size, since
// we:
// 1. Return a boxed future, so calling `async` functions don't need to inline this one.
// 2. Don't have to monomorphize for each telemetry event implementation.
fn send_telemetry_event_internal(
&self,
event: Event,
settings_snapshot: PrivacySettingsSnapshot,
) -> impl Future<Output = Result<()>> + '_ {
let work = async move {
if settings_snapshot.should_disable_telemetry() {
log::info!("Not sending telemetry event because telemetry is disabled.");
return Result::Ok(());
}
#[cfg(not(target_family = "wasm"))]
if FeatureFlag::SendTelemetryToFile.is_enabled() {
self.persist_events_to_telemetry_log_file(vec![event.clone()])?;
}
if !(ChannelState::is_release_bundle()
|| FeatureFlag::WithSandboxTelemetry.is_enabled())
{
return Result::Ok(());
}
let rudder_batch = vec![event.to_rudder_batch_message()];
let result = self
.send_batch_messages_to_rudder(rudder_batch, settings_snapshot)
.await;
// This is only conditionally compiled because `is_connect` is not
// available on wasm. If additional checks are made against the
// `reqwest::Error`, this condition should be performed specifically
// against `is_connect` and not the whole loop.
#[cfg(not(target_family = "wasm"))]
if let Err(error) = &result {
for cause in error.chain() {
if let Some(err) = cause.downcast_ref::<reqwest::Error>() {
if err.is_connect() {
log::warn!("Failed to send telemetry event: {error}");
return Ok(());
}
}
}
}
result
};
// On WASM, the work future is non-Send, because the HTTP request future contains a reference to a JS
// value (which is fine, since our WASM executor is single-threaded). On all other platforms, we must
// return a Send future in order to use the background executor.
cfg_if::cfg_if! {
if #[cfg(target_family = "wasm")] {
work.boxed_local()
} else {
work.boxed()
}
}
}
/// Send a batch of RudderStack messages to their HTTP API.
/// Note that the rudderanalytics SDK provides a client, but we don't
/// use it for a few reasons:
/// 1. It only supports a blocking HTTP client instead of an async one
/// 2. We want to use our own HTTP client which has before/after request logging hooks
#[cfg_attr(target_family = "wasm", allow(clippy::question_mark))]
async fn send_batch_messages_to_rudder(
&self,
messages: Vec<RudderBatchMessageWithMetadata>,
settings_snapshot: PrivacySettingsSnapshot,
) -> Result<()> {
if messages.is_empty() {
log::debug!("Dropping empty RudderStack telemetry batch");
return Ok(());
}
if settings_snapshot.should_disable_telemetry() {
log::info!("Not sending batched messages because telemetry is disabled.");
return Ok(());
}
log::info!("Start to send telemetry events to RudderStack");
let (mut messages_with_ugc, messages_without_ugc): (Vec<_>, Vec<_>) = messages
.into_iter()
.partition(|message| message.contains_ugc);
// If we shouldn't collect UGC telemetry, forcibly clear any messages with UGC before trying to send.
if !settings_snapshot.should_collect_ai_ugc_telemetry() {
messages_with_ugc.clear();
}
for (messages, rudder_stack_destination) in [
(
messages_with_ugc,
ChannelState::rudderstack_ugc_destination(),
),
(
messages_without_ugc,
ChannelState::rudderstack_non_ugc_destination(),
),
] {
if messages.is_empty() {
continue;
}
// Note that timestamp and context are already included in the individual RudderBatchMessages
// and these are the most important ones,
// but we also add them to the RudderMessage::Batch wrapper.
let rudder_message = RudderMessage::Batch(RudderBatch {
batch: messages
.into_iter()
.map(|message| message.message)
.collect(),
original_timestamp: Some(Utc::now()),
..Default::default()
});
if let Err(e) = self
.send_rudder_request(rudder_message, rudder_stack_destination)
.await
{
// Don't treat a connection issue as an error as these are outside of our control.
//
// This is only conditionally compiled because `is_connect` is not
// available on wasm. If additional checks are made against the
// `reqwest::Error`, this condition should be performed specifically
// against `is_connect` and not the whole loop.
#[cfg(not(target_family = "wasm"))]
for cause in e.chain() {
if let Some(err) = cause.downcast_ref::<reqwest::Error>() {
if err.is_connect() {
log::warn!("Failed to send event to RudderStack: {e}");
return Ok(());
}
}
}
return Err(e);
}
}
_user_id: Option<crate::auth::UserUid>,
_anonymous_id: String,
_event: impl galaxy_core::telemetry::TelemetryEvent,
_settings_snapshot: crate::settings::PrivacySettingsSnapshot,
) -> anyhow::Result<()> {
Ok(())
}
@@ -252,3 +61,8 @@ impl TelemetryApi {
Ok(())
}
}
#[allow(dead_code)]
pub fn clear_event_queue() {
let _ = galaxyui::telemetry::flush_events();
}
+250
View File
@@ -1 +1,251 @@
#![allow(dead_code)]
//! Module that contains RudderStack API message types.
//! This is directly copied from the RudderStack Rust SDK: https://github.com/rudderlabs/rudder-sdk-rust/blob/master/src/message.rs
//! We do not use the SDK directly because it unconditionally uses a blocking HTTP client, which we don't want for a few reasons:
//! 1. The blocking HTTP client is not allowed when compiling for WASM, so the crate itself cannot be compiled for WASM
//! 2. An async HTTP client is more efficient
//! 3. We want to use our own HTTP client which has before/after request logging hooks
//! We can consider using the SDK if it adds support for an async HTTP client, tracked by this issue: https://github.com/rudderlabs/rudder-sdk-rust/issues/23
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::auth::UserUid;
/// An enum containing all values which may be sent to RudderStack's API.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Message {
Identify(Identify),
Track(Track),
Page(Page),
Screen(Screen),
Group(Group),
Alias(Alias),
Batch(Batch),
}
/// An identify event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Identify {
/// The user id associated with this message.
#[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
pub user_id: Option<UserUid>,
/// The anonymous user id associated with this message.
#[serde(rename = "anonymousId", skip_serializing_if = "Option::is_none")]
pub anonymous_id: Option<String>,
/// The traits to assign to the user.
#[serde(skip_serializing_if = "Option::is_none")]
pub traits: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// A track event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Track {
/// The user id associated with this message.
#[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
pub user_id: Option<UserUid>,
/// The anonymous user id associated with this message.
#[serde(rename = "anonymousId", skip_serializing_if = "Option::is_none")]
pub anonymous_id: Option<String>,
/// The name of the event being tracked.
pub event: String,
/// The properties associated with the event.
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// A page event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Page {
/// The user id associated with this message.
#[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
pub user_id: Option<UserUid>,
/// The anonymous user id associated with this message.
#[serde(rename = "anonymousId", skip_serializing_if = "Option::is_none")]
pub anonymous_id: Option<String>,
/// The name of the page being tracked.
pub name: String,
/// The properties associated with the event.
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// A screen event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Screen {
/// The user id associated with this message.
#[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
pub user_id: Option<UserUid>,
/// The anonymous user id associated with this message.
#[serde(rename = "anonymousId", skip_serializing_if = "Option::is_none")]
pub anonymous_id: Option<String>,
/// The name of the screen being tracked.
pub name: String,
/// The properties associated with the event.
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// A group event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Group {
/// The user id associated with this message.
#[serde(rename = "userId", skip_serializing_if = "Option::is_none")]
pub user_id: Option<UserUid>,
/// The anonymous user id associated with this message.
#[serde(rename = "anonymousId", skip_serializing_if = "Option::is_none")]
pub anonymous_id: Option<String>,
/// The group the user is being associated with.
#[serde(rename = "groupId")]
pub group_id: String,
/// The traits to assign to the group.
#[serde(skip_serializing_if = "Option::is_none")]
pub traits: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// An alias event.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Alias {
/// The user id associated with this message.
#[serde(rename = "userId")]
pub user_id: UserUid,
/// The user's previous ID.
#[serde(rename = "previousId")]
pub previous_id: String,
/// The traits to assign to the alias.
#[serde(skip_serializing_if = "Option::is_none")]
pub traits: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
}
/// A batch of events.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, Default)]
pub struct Batch {
/// The batch of messages to send.
pub batch: Vec<BatchMessageItem>,
/// Context associated with this message.
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<Value>,
/// Integrations to route this message to.
#[serde(skip_serializing_if = "Option::is_none")]
pub integrations: Option<Value>,
/// The timestamp associated with this message.
#[serde(rename = "originalTimestamp", skip_serializing_if = "Option::is_none")]
pub original_timestamp: Option<DateTime<Utc>>,
}
/// An enum containing all messages which may be placed inside a batch.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum BatchMessageItem {
#[serde(rename = "identify")]
Identify(Identify),
#[serde(rename = "track")]
Track(Track),
#[serde(rename = "page")]
Page(Page),
#[serde(rename = "screen")]
Screen(Screen),
#[serde(rename = "group")]
Group(Group),
#[serde(rename = "alias")]
Alias(Alias),
}
/// Metadata about a batch sent to Rudderstack and whether it contains user generated content.
pub struct BatchMessage {
pub message: BatchMessageItem,
pub contains_ugc: bool,
}
@@ -1,3 +1,5 @@
#![allow(dead_code)]
//! Best-effort secret redaction for telemetry payloads.
//!
//! Unlike the AI-side secret redaction in `app/src/ai/blocklist/block/secret_redaction.rs`,
@@ -33,3 +35,95 @@ lazy_static! {
/// custom patterns).
static ref TELEMETRY_SECRETS_REGEX: RwLock<Regex> = RwLock::new(build_default_regex());
}
/// Builds a regex containing only the default patterns. Used to seed the static
/// regex before the privacy settings are loaded.
fn build_default_regex() -> Regex {
let patterns: Vec<&str> = DEFAULT_REGEXES_WITH_NAMES
.iter()
.map(|d| d.pattern)
.collect();
Regex::new_many(&patterns).expect("default secret patterns should compile")
}
/// Rebuilds [`TELEMETRY_SECRETS_REGEX`] from the user's and enterprise's secret
/// regex lists, layered on top of the default patterns. The default patterns are
/// always included, so redaction works even when the user has not configured any
/// custom patterns.
pub fn update_telemetry_secrets_regex<'a, U, E>(user_secrets: U, enterprise_secrets: E)
where
U: IntoIterator<Item = &'a regex::Regex>,
E: IntoIterator<Item = &'a regex::Regex>,
{
let patterns = compose_patterns(
user_secrets.into_iter().map(regex::Regex::as_str),
enterprise_secrets.into_iter().map(regex::Regex::as_str),
);
match Regex::new_many(&patterns) {
Ok(regex) => *TELEMETRY_SECRETS_REGEX.write() = regex,
Err(err) => log::error!("Failed to build telemetry secrets regex: {err:?}"),
}
}
/// Composes the full list of patterns to compile into the telemetry regex,
/// ordered enterprise → user → defaults, with later occurrences of an already-
/// seen pattern string deduped out.
fn compose_patterns<'a>(
user: impl Iterator<Item = &'a str>,
enterprise: impl Iterator<Item = &'a str>,
) -> Vec<&'a str> {
let mut seen: HashSet<&str> = HashSet::new();
let mut patterns: Vec<&str> = Vec::new();
let all = enterprise
.chain(user)
.chain(DEFAULT_REGEXES_WITH_NAMES.iter().map(|d| d.pattern));
for pattern in all {
if seen.insert(pattern) {
patterns.push(pattern);
}
}
patterns
}
/// Replaces every detected secret in `input` with a run of asterisks of the same
/// byte length. Overlapping matches (which can occur when multiple patterns match
/// the same region) are merged before replacement, so each character is replaced
/// at most once.
pub fn redact_secrets_in_string(input: &mut String) {
let ranges: Vec<Range<usize>> = {
let regex = TELEMETRY_SECRETS_REGEX.read();
regex.find_iter(input.as_str()).map(|m| m.range()).collect()
};
replace_byte_ranges_with_asterisks(input, ranges);
}
/// Replaces each byte range in `input` with a run of asterisks of the same byte
/// length. Handles overlapping ranges by merging them first, and replaces from
/// the end of the string so earlier byte indices stay valid as we mutate.
fn replace_byte_ranges_with_asterisks(input: &mut String, mut ranges: Vec<Range<usize>>) {
if ranges.is_empty() {
return;
}
// Sort and merge overlapping ranges so we don't double-replace.
ranges.sort_by_key(|r| r.start);
let mut merged: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
for range in ranges {
match merged.last_mut() {
Some(last) if range.start <= last.end => last.end = last.end.max(range.end),
_ => merged.push(range),
}
}
// Replace from the end of the string so earlier byte indices stay valid.
for range in merged.into_iter().rev() {
let len = range.end - range.start;
input.replace_range(range, &REDACTION_REPLACEMENT_CHARACTER.repeat(len));
}
}
/// Walks a [`Value`] and runs [`redact_secrets_in_string`] on every string within
/// it. Non-string scalars (numbers, booleans, nulls) are left untouched.
pub fn redact_secrets_in_value(value: &mut Value) {
match value {
Value::String(s) => redact_secrets_in_string(s),
Value::Array(arr) => arr.iter_mut().for_each(redact_secrets_in_value),
Value::Object(obj) => obj.values_mut().for_each(redact_secrets_in_value),
Value::Null | Value::Bool(_) | Value::Number(_) => {}
}
}
#[cfg(test)]
#[path = "secret_redaction_tests.rs"]
mod tests;