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:
@@ -3,6 +3,8 @@ use std::sync::mpsc::{sync_channel, Receiver};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cloud_object_client::{MockObjectClient, ObjectClient};
|
||||
use galaxy_core::execution_mode::{AppExecutionMode, ExecutionMode};
|
||||
use galaxyui::{App, ModelHandle, SingletonEntity};
|
||||
use settings::manager::SettingsManager;
|
||||
|
||||
use super::update_manager::UpdateManager;
|
||||
|
||||
@@ -11,21 +11,19 @@ pub use cloud_object_client::InitialLoadResponse;
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use futures::stream::AbortHandle;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::report_error;
|
||||
use galaxy_graphql::mcp_gallery_template::MCPGalleryTemplate;
|
||||
use galaxy_graphql::object_permissions::AccessLevel;
|
||||
use galaxy_graphql::scalars::time::ServerTimestamp;
|
||||
use galaxyui::r#async::{FutureId, Timer};
|
||||
use galaxyui::{duration_with_jitter, AppContext};
|
||||
use galaxyui::{Entity, ModelContext, RequestState, RetryOption, SingletonEntity};
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
use galaxy_core::report_error;
|
||||
use galaxy_util::sync::Condition;
|
||||
use galaxyui::r#async::{FutureId, Timer};
|
||||
use galaxyui::{
|
||||
duration_with_jitter, AppContext, Entity, ModelContext, ModelHandle, RequestState, RetryOption,
|
||||
SingletonEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use lazy_static::lazy_static;
|
||||
use regex::Regex;
|
||||
|
||||
use super::listener::ObjectUpdateMessage;
|
||||
use crate::ai::agent::conversation::AIConversationId;
|
||||
|
||||
@@ -6,11 +6,11 @@ use chrono::{DateTime, Utc};
|
||||
use cloud_object_client::MockObjectClient;
|
||||
use cloud_object_models::JsonSerializer;
|
||||
use futures_lite::future;
|
||||
use settings::{RespectUserSyncSetting, SyncToCloud};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_graphql::object_permissions::AccessLevel;
|
||||
use galaxy_graphql::scalars::time::ServerTimestamp;
|
||||
use galaxyui::{App, ModelHandle, SingletonEntity};
|
||||
use settings::SyncToCloud;
|
||||
|
||||
use super::{GetCloudObjectResponse, InitialLoadResponse, UpdateManager};
|
||||
use crate::ai::cloud_environments::{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod schema;
|
||||
|
||||
pub use galaxy_graphql::client::{get_request_context, get_user_facing_error_message, GraphQLError};
|
||||
pub use galaxy_graphql::client::{
|
||||
get_request_context, get_user_facing_error_message, GraphQLError,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
pub mod util;
|
||||
use anyhow::{bail, Result};
|
||||
pub use util::{action_type_to_gql_action_type, object_action_history_from_gql};
|
||||
use galaxy_graphql::generic_string_object::GenericStringObjectFormat;
|
||||
use galaxy_graphql::mutations::update_generic_string_object::{
|
||||
GenericStringObjectUpdate, UpdateGenericStringObjectResult,
|
||||
};
|
||||
pub use util::{action_type_to_gql_action_type, object_action_history_from_gql};
|
||||
use warp_graphql::object::ObjectUpdateSuccess;
|
||||
|
||||
use crate::cloud_object::{
|
||||
|
||||
@@ -25,15 +25,6 @@ use auth::AuthClient;
|
||||
use block::BlockClient;
|
||||
use channel_versions::ChannelVersions;
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use instant::Instant;
|
||||
use managed_mcp::ManagedMcpClient;
|
||||
use object::ObjectClient;
|
||||
use parking_lot::Mutex;
|
||||
use referral::ReferralsClient;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use team::TeamClient;
|
||||
use url::Url;
|
||||
use galaxy_core::context_flag::ContextFlag;
|
||||
use galaxy_core::errors::{register_error, AnyhowErrorExt, ErrorExt};
|
||||
use galaxy_core::telemetry::TelemetryEvent;
|
||||
@@ -46,6 +37,15 @@ use galaxy_server_client::iap::{IapManager, IapState};
|
||||
use galaxy_server_client::network_logging::NetworkLogModel;
|
||||
use galaxyui::r#async::BoxFuture;
|
||||
use galaxyui::{Entity, ModelContext, SingletonEntity};
|
||||
use instant::Instant;
|
||||
use managed_mcp::ManagedMcpClient;
|
||||
use object::ObjectClient;
|
||||
use parking_lot::Mutex;
|
||||
use referral::ReferralsClient;
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use team::TeamClient;
|
||||
use url::Url;
|
||||
use workspace::WorkspaceClient;
|
||||
|
||||
use super::experiments::{ServerExperiment, ServerExperiments};
|
||||
|
||||
@@ -13,11 +13,6 @@ use base64::Engine;
|
||||
use chrono::{DateTime, Utc};
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_core::{features::FeatureFlag, report_error};
|
||||
use itertools::Itertools;
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use prost::Message;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::report_error;
|
||||
use galaxy_graphql::ai::{AgentTaskState, PlatformErrorCode};
|
||||
@@ -110,6 +105,10 @@ use galaxy_graphql::queries::task_git_credentials::{
|
||||
TaskGitCredentials, TaskGitCredentialsInput, TaskGitCredentialsResult,
|
||||
TaskGitCredentialsVariables,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use prost::Message;
|
||||
use warp_multi_agent_api::ConversationData;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
@@ -1121,6 +1120,8 @@ pub struct UpdateMemoryResponse {
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
pub trait AIClient: 'static + Send + Sync {
|
||||
fn base_client(&self) -> std::sync::Arc<galaxy_server_client::base_client::BaseClient>;
|
||||
|
||||
async fn generate_commands_from_natural_language(
|
||||
&self,
|
||||
prompt: String,
|
||||
@@ -1602,6 +1603,10 @@ fn convert_upload_field(
|
||||
#[cfg_attr(not(target_family = "wasm"), async_trait)]
|
||||
#[cfg_attr(target_family = "wasm", async_trait(?Send))]
|
||||
impl AIClient for ServerApi {
|
||||
fn base_client(&self) -> std::sync::Arc<galaxy_server_client::base_client::BaseClient> {
|
||||
self.base_client.clone()
|
||||
}
|
||||
|
||||
async fn generate_commands_from_natural_language(
|
||||
&self,
|
||||
prompt: String,
|
||||
@@ -3306,6 +3311,10 @@ fn convert_usage_metadata(
|
||||
credits_spent_for_last_block: None,
|
||||
token_usage: vec![],
|
||||
tool_usage_metadata: Default::default(),
|
||||
total_cache_read_tokens: 0,
|
||||
total_cache_write_tokens: 0,
|
||||
total_cache_miss_tokens: 0,
|
||||
total_cost_cents: 0.0,
|
||||
context_window_segments: context_window_segments.iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use thiserror::Error;
|
||||
#[cfg(test)]
|
||||
pub use galaxy_server_client::auth::MockAuthClient;
|
||||
pub use galaxy_server_client::auth::{
|
||||
AuthClient, FetchUserResult, MintCustomTokenError, SyncedUserSettings, UserAuthenticationError,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
/// Error type when creating anonymous users.
|
||||
|
||||
@@ -4,8 +4,6 @@ use anyhow::anyhow;
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use galaxy_core::channel::{Channel, ChannelState};
|
||||
use galaxy_graphql::mutations::share_block::{
|
||||
BlockInput, ShareBlock, ShareBlockResult, ShareBlockVariables,
|
||||
@@ -16,6 +14,8 @@ use galaxy_graphql::mutations::unshare_block::{
|
||||
use galaxy_graphql::queries::get_blocks_for_user::{
|
||||
Block as GqlBlock, GetBlocksForUser, GetBlocksForUserVariables,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
|
||||
use super::ServerApi;
|
||||
use crate::ai::generate_block_title::api::{GenerateBlockTitleRequest, GenerateBlockTitleResponse};
|
||||
|
||||
@@ -50,7 +50,7 @@ fn report_shutdown_clean_serializes_without_error() {
|
||||
|
||||
#[test]
|
||||
fn report_shutdown_abnormal_serializes_with_error() {
|
||||
|
||||
use super::ReportShutdownRequest;
|
||||
let request = ReportShutdownRequest::abnormal("oom".to_string(), "out of memory".to_string());
|
||||
let json = serde_json::to_value(&request).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
use galaxy_graphql::mutations::create_simple_integration::{
|
||||
CreateSimpleIntegration, CreateSimpleIntegrationOutput, CreateSimpleIntegrationResult,
|
||||
CreateSimpleIntegrationVariables, SimpleIntegrationConfig,
|
||||
@@ -32,6 +30,8 @@ use galaxy_graphql::queries::user_repo_auth_status::{
|
||||
RepoInput as UserRepoAuthStatusRepoInput, UserRepoAuthStatus, UserRepoAuthStatusInput,
|
||||
UserRepoAuthStatusOutput, UserRepoAuthStatusResult, UserRepoAuthStatusVariables,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use mockall::automock;
|
||||
|
||||
use super::ServerApi;
|
||||
use crate::channel::ChannelState;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
use galaxy_core::channel::ChannelState;
|
||||
use galaxy_graphql::mutations::send_referral_invite_emails::{
|
||||
SendReferralInviteEmails, SendReferralInviteEmailsResult, SendReferralInviteEmailsVariables,
|
||||
};
|
||||
use galaxy_graphql::queries::get_referral_info::{GetReferralInfo, GetReferralInfoVariables};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
|
||||
use super::ServerApi;
|
||||
use crate::server::graphql::{get_request_context, get_user_facing_error_message};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
use galaxy_graphql::mutations::add_invite_link_domain_restriction::{
|
||||
AddInviteLinkDomainRestriction, AddInviteLinkDomainRestrictionInput,
|
||||
AddInviteLinkDomainRestrictionResult, AddInviteLinkDomainRestrictionVariables,
|
||||
@@ -56,6 +54,8 @@ use galaxy_graphql::queries::get_discoverable_teams::{
|
||||
use galaxy_graphql::queries::get_workspaces_metadata_for_user::{
|
||||
GetWorkspacesMetadataForUser, GetWorkspacesMetadataForUserVariables, PricingInfoResult,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
|
||||
use super::ServerApi;
|
||||
use crate::auth::UserUid;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use cynic::{MutationBuilder, QueryBuilder};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
use galaxy_graphql::error::UserFacingErrorInterface;
|
||||
use galaxy_graphql::mutations::purchase_addon_credits::{
|
||||
PurchaseAddonCredits, PurchaseAddonCreditsInput, PurchaseAddonCreditsResult,
|
||||
@@ -20,6 +18,8 @@ use galaxy_graphql::mutations::update_workspace_settings::{
|
||||
use galaxy_graphql::queries::get_ai_overages_for_workspace::{
|
||||
GetAiOveragesForWorkspace, GetAiOveragesForWorkspaceVariables, UserResult,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use mockall::{automock, predicate::*};
|
||||
|
||||
use super::team::TeamClient;
|
||||
use super::ServerApi;
|
||||
|
||||
@@ -8,15 +8,11 @@ use chrono::{DateTime, Utc};
|
||||
pub use cloud_objects::cloud_object::SerializedModel;
|
||||
use derivative::Derivative;
|
||||
use galaxy_graphql::scalars::time::ServerTimestamp;
|
||||
use galaxyui::{
|
||||
r#async::FutureId, Entity, ModelContext, RequestState, RetryOption, SingletonEntity,
|
||||
};
|
||||
use galaxyui::r#async::FutureId;
|
||||
use galaxyui::{Entity, ModelContext, RequestState, RetryOption, SingletonEntity};
|
||||
use http::StatusCode;
|
||||
use lazy_static::lazy_static;
|
||||
use uuid::Uuid;
|
||||
use warp_graphql::scalars::time::ServerTimestamp;
|
||||
use warpui::r#async::FutureId;
|
||||
use warpui::{Entity, ModelContext, RequestState, RetryOption, SingletonEntity};
|
||||
|
||||
use super::graphql::GraphQLError;
|
||||
use super::ids::{ClientId, HashableId, ObjectUid, ServerId, SyncId, ToServerId};
|
||||
@@ -1812,19 +1808,17 @@ impl SyncQueue {
|
||||
);
|
||||
}
|
||||
}
|
||||
QueueItem::UpdateWorkflow { model, .. } => {
|
||||
// Only update the workflow if it depends on this dependency.
|
||||
QueueItem::UpdateWorkflow { model, .. }
|
||||
if self
|
||||
.queue_dependencies
|
||||
.get(item_id)
|
||||
.is_some_and(|deps| deps.contains(dependency))
|
||||
{
|
||||
let workflow_model = Arc::make_mut(model);
|
||||
workflow_model.data.replace_object_id(
|
||||
SyncId::ClientId(client_id),
|
||||
SyncId::from(server_id),
|
||||
);
|
||||
}
|
||||
.is_some_and(|deps| deps.contains(dependency)) =>
|
||||
{
|
||||
let workflow_model = Arc::make_mut(model);
|
||||
workflow_model.data.replace_object_id(
|
||||
SyncId::ClientId(client_id),
|
||||
SyncId::from(server_id),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_json::{json, Value};
|
||||
use galaxy_core::channel::{Channel, ChannelState};
|
||||
use galaxy_core::execution_mode;
|
||||
use galaxyui::telemetry::EventPayload;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::telemetry::rudder_message::{
|
||||
BatchMessage as RudderBatchMessage, BatchMessageItem as RudderBatchMessageItem,
|
||||
|
||||
Reference in New Issue
Block a user