Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 26 additions & 74 deletions crates/openshell-core/src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,60 +193,6 @@ impl Default for TelemetryComputeDriver {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProviderProfile {
Anthropic,
Claude,
Codex,
Copilot,
Deepinfra,
Github,
Gitlab,
Nvidia,
Openai,
Opencode,
Outlook,
Custom,
}

impl ProviderProfile {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Anthropic => "anthropic",
Self::Claude => "claude",
Self::Codex => "codex",
Self::Copilot => "copilot",
Self::Deepinfra => "deepinfra",
Self::Github => "github",
Self::Gitlab => "gitlab",
Self::Nvidia => "nvidia",
Self::Openai => "openai",
Self::Opencode => "opencode",
Self::Outlook => "outlook",
Self::Custom => "custom",
}
}

#[must_use]
pub fn from_raw(raw: &str) -> Self {
match raw.trim().to_ascii_lowercase().as_str() {
"anthropic" => Self::Anthropic,
"claude" | "claude-code" => Self::Claude,
"codex" => Self::Codex,
"copilot" => Self::Copilot,
"deepinfra" => Self::Deepinfra,
"github" | "gh" => Self::Github,
"gitlab" | "glab" => Self::Gitlab,
"nvidia" => Self::Nvidia,
"openai" => Self::Openai,
"opencode" => Self::Opencode,
"outlook" => Self::Outlook,
_ => Self::Custom,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DenyGroup {
Bypass,
Expand Down Expand Up @@ -487,22 +433,24 @@ pub fn emit_lifecycle(
);
}

pub fn emit_provider_lifecycle(
operation: LifecycleOperation,
outcome: TelemetryOutcome,
provider_profile: ProviderProfile,
) {
pub fn emit_provider_lifecycle(operation: LifecycleOperation, outcome: TelemetryOutcome) {
emit_event(
"openshell_provider_lifecycle_event",
json!({
"nvidiaSource": SOURCE.as_str(),
"operation": operation.as_str(),
"outcome": outcome.as_str(),
"providerProfile": provider_profile.as_str(),
}),
provider_lifecycle_parameters(operation, outcome),
);
}

fn provider_lifecycle_parameters(
operation: LifecycleOperation,
outcome: TelemetryOutcome,
) -> Value {
json!({
"nvidiaSource": SOURCE.as_str(),
"operation": operation.as_str(),
"outcome": outcome.as_str(),
})
}

pub fn emit_sandbox_create(
outcome: TelemetryOutcome,
requested_gpu: bool,
Expand Down Expand Up @@ -704,16 +652,24 @@ mod tests {

#[test]
fn telemetry_validation_maps_privacy_sensitive_strings_to_safe_buckets() {
assert_eq!(
ProviderProfile::from_raw("corp-llm-prod"),
ProviderProfile::Custom
);
assert_eq!(
DenyGroup::from_raw("host=private.example"),
DenyGroup::Unknown
);
}

#[test]
fn provider_lifecycle_parameters_exclude_profile_identity() {
assert_eq!(
provider_lifecycle_parameters(LifecycleOperation::Create, TelemetryOutcome::Success),
json!({
"nvidiaSource": "openshell",
"operation": "create",
"outcome": "success",
})
);
}

#[test]
fn telemetry_enums_serialize_to_expected_strings() {
assert_eq!(LifecycleResource::SandboxPolicy.as_str(), "sandbox_policy");
Expand Down Expand Up @@ -757,11 +713,7 @@ mod disabled_tests {
LifecycleOperation::Create,
TelemetryOutcome::Success,
);
emit_provider_lifecycle(
LifecycleOperation::Create,
TelemetryOutcome::Success,
ProviderProfile::Custom,
);
emit_provider_lifecycle(LifecycleOperation::Create, TelemetryOutcome::Success);
emit_sandbox_create(
TelemetryOutcome::Success,
false,
Expand Down
157 changes: 9 additions & 148 deletions crates/openshell-server/src/grpc/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ use openshell_core::proto::{
ProviderProfile, ProviderProfileCredential, Sandbox, StaticCredentialBinding,
StaticCredentialEndpointBinding,
};
use openshell_core::telemetry::{
LifecycleOperation, ProviderProfile as TelemetryProviderProfile, TelemetryOutcome,
};
use openshell_core::telemetry::{LifecycleOperation, TelemetryOutcome, emit_provider_lifecycle};
use openshell_policy::ProviderPolicyLayer;
use prost::Message;
use sha2::{Digest, Sha256};
Expand Down Expand Up @@ -2532,11 +2530,7 @@ pub(super) async fn handle_create_provider(
.await?
.ensure_active()?;
let Some(mut provider) = req.provider else {
emit_provider_lifecycle(
"custom",
LifecycleOperation::Create,
TelemetryOutcome::Failure,
);
emit_provider_lifecycle(LifecycleOperation::Create, TelemetryOutcome::Failure);
return Err(Status::invalid_argument("provider is required"));
};
if let Some(metadata) = provider.metadata.as_mut() {
Expand All @@ -2547,7 +2541,6 @@ pub(super) async fn handle_create_provider(
"provider.credential_handles is internal gateway state and cannot be supplied",
));
}
let provider_type = provider.r#type.clone();
let _sandbox_sync_guard = state.compute.sandbox_sync_guard().await.map_err(|error| {
super::persistence_error_to_status(error, "acquire provider mutation lock")
})?;
Expand All @@ -2570,22 +2563,14 @@ pub(super) async fn handle_create_provider(
.await;
match result {
Ok(provider) => {
emit_provider_lifecycle(
&provider.r#type,
LifecycleOperation::Create,
TelemetryOutcome::Success,
);
emit_provider_lifecycle(LifecycleOperation::Create, TelemetryOutcome::Success);
Ok(Response::new(ProviderResponse {
provider: Some(provider),
..Default::default()
}))
}
Err(err) => {
emit_provider_lifecycle(
&provider_type,
LifecycleOperation::Create,
TelemetryOutcome::Failure,
);
emit_provider_lifecycle(LifecycleOperation::Create, TelemetryOutcome::Failure);
Err(err)
}
}
Expand Down Expand Up @@ -3846,14 +3831,9 @@ pub(super) async fn handle_update_provider(
super::persistence_error_to_status(error, "acquire provider mutation lock")
})?;
let Some(mut provider) = req.provider else {
emit_provider_lifecycle(
"custom",
LifecycleOperation::Update,
TelemetryOutcome::Failure,
);
emit_provider_lifecycle(LifecycleOperation::Update, TelemetryOutcome::Failure);
return Err(Status::invalid_argument("provider is required"));
};
let provider_type = provider.r#type.clone();
provider
.credential_expiration_times
.extend(req.credential_expiration_times);
Expand Down Expand Up @@ -3900,23 +3880,15 @@ pub(super) async fn handle_update_provider(
.await?,
);
}
emit_provider_lifecycle(
&provider.r#type,
LifecycleOperation::Update,
TelemetryOutcome::Success,
);
emit_provider_lifecycle(LifecycleOperation::Update, TelemetryOutcome::Success);
Ok(Response::new(ProviderResponse {
provider: Some(provider),
target_receipts,
mutation_id,
}))
}
Err(err) => {
emit_provider_lifecycle(
&provider_type,
LifecycleOperation::Update,
TelemetryOutcome::Failure,
);
emit_provider_lifecycle(LifecycleOperation::Update, TelemetryOutcome::Failure);
Err(err)
}
}
Expand Down Expand Up @@ -5048,7 +5020,6 @@ pub(super) async fn handle_delete_provider(
.await?
.name;
let name = req.name;
let provider_profile = provider_profile_for_name(state.store.as_ref(), &workspace, &name).await;
let result = delete_provider_record_with_credentials(
state.store.as_ref(),
&workspace,
Expand All @@ -5059,74 +5030,18 @@ pub(super) async fn handle_delete_provider(
match result {
Ok(deleted) => {
let outcome = TelemetryOutcome::from_success(deleted || req.allow_missing);
emit_provider_profile_lifecycle(
provider_profile.unwrap_or(TelemetryProviderProfile::Custom),
LifecycleOperation::Delete,
outcome,
);
emit_provider_lifecycle(LifecycleOperation::Delete, outcome);
Ok(Response::new(DeleteProviderResponse {
outcome: super::deletion_outcome(deleted, req.allow_missing, "provider")?,
}))
}
Err(err) => {
emit_provider_profile_lifecycle(
provider_profile.unwrap_or(TelemetryProviderProfile::Custom),
LifecycleOperation::Delete,
TelemetryOutcome::Failure,
);
emit_provider_lifecycle(LifecycleOperation::Delete, TelemetryOutcome::Failure);
Err(err)
}
}
}

fn emit_provider_lifecycle(
provider_type: &str,
operation: LifecycleOperation,
outcome: TelemetryOutcome,
) {
let provider_profile = telemetry_provider_profile(provider_type);
emit_provider_profile_lifecycle(provider_profile, operation, outcome);
}

fn emit_provider_profile_lifecycle(
provider_profile: TelemetryProviderProfile,
operation: LifecycleOperation,
outcome: TelemetryOutcome,
) {
openshell_core::telemetry::emit_provider_lifecycle(operation, outcome, provider_profile);
}

async fn provider_profile_for_name(
store: &Store,
workspace: &str,
name: &str,
) -> Option<TelemetryProviderProfile> {
store
.get_message_by_name::<Provider>(workspace, name)
.await
.ok()
.flatten()
.map(|provider| telemetry_provider_profile(&provider.r#type))
}

/// Bucket a provider type for telemetry.
///
/// Matches the profile ID exactly. Any ID without a bucket, including every
/// operator-authored profile, reports as `Custom`.
fn telemetry_provider_profile(provider_type: &str) -> TelemetryProviderProfile {
match normalize_profile_id(provider_type).as_deref() {
Some("anthropic") => TelemetryProviderProfile::Anthropic,
Some("claude-code") => TelemetryProviderProfile::Claude,
Some("codex") => TelemetryProviderProfile::Codex,
Some("copilot") => TelemetryProviderProfile::Copilot,
Some("deepinfra") => TelemetryProviderProfile::Deepinfra,
Some("github") => TelemetryProviderProfile::Github,
Some("nvidia") => TelemetryProviderProfile::Nvidia,
Some("openai") => TelemetryProviderProfile::Openai,
_ => TelemetryProviderProfile::Custom,
}
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -5243,60 +5158,6 @@ mod tests {
validate_provider_create_credentials(&profile, &provider).unwrap();
}

#[test]
fn telemetry_provider_profile_maps_unknown_to_custom() {
assert_eq!(
telemetry_provider_profile("CLAUDE-CODE"),
TelemetryProviderProfile::Claude
);
// A legacy alias is not a profile ID, so it buckets as custom.
assert_eq!(
telemetry_provider_profile("claude"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("github"),
TelemetryProviderProfile::Github
);
// Legacy aliases are not profile IDs.
assert_eq!(
telemetry_provider_profile("gh"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("glab"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("gitlab"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("opencode"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("outlook"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("generic"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("unknown-private"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("acme-internal"),
TelemetryProviderProfile::Custom
);
assert_eq!(
telemetry_provider_profile("corp-llm-prod"),
TelemetryProviderProfile::Custom
);
}

#[test]
fn dynamic_credentials_expand_endpoint_audience_overrides() {
let service_audiences = [
Expand Down
Loading
Loading