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,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::{
+3 -1
View File
@@ -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 -1
View File
@@ -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::{
+9 -9
View File
@@ -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};
+14 -5
View File
@@ -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 -1
View File
@@ -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.
+2 -2
View File
@@ -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!(
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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};
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+10 -16
View File
@@ -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),
);
}
_ => {}
}
+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;
+3 -1
View File
@@ -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,