Skip to content
This repository has been archived by the owner on Dec 21, 2024. It is now read-only.

chore: add sentry integration #592

Closed
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
744 changes: 727 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ repository = "https://github.com/rivet-gg/cli"
name = "rivet"
path = "src/main.rs"

[features]
default = ["sentry"]
sentry = []

[dependencies]
clap = { version = "4.5.9", features = ["derive"] }
toolchain = { version = "0.1.0", path = "../toolchain", package = "rivet-toolchain" }
Expand All @@ -23,6 +27,13 @@ base64 = "0.22.1"
kv-str = { version = "0.1.0", path = "../kv-str" }
inquire = "0.7.5"
webbrowser = "1.0.2"
sentry = { version = "0.34.0", features = ["anyhow"] }
sysinfo = "0.32.0"
ctrlc = "3.4.5"

[dependencies.async-posthog]
git = "https://github.com/rivet-gg/posthog-rs"
rev = "ef4e80e"

[build-dependencies]
anyhow = "1.0"
Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/commands/actor/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use toolchain::{
};
use uuid::Uuid;


#[derive(ValueEnum, Clone)]
enum NetworkMode {
Bridge,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/actor/destroy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::*;
use clap::Parser;
use toolchain::rivet_api::apis;
use toolchain::{errors, rivet_api::apis};
use uuid::Uuid;

#[derive(Parser)]
Expand All @@ -21,7 +21,8 @@ impl Opts {

let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?;

let actor_id = Uuid::parse_str(&self.id).context("invalid id uuid")?;
let actor_id =
Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?;

apis::actor_api::actor_destroy(
&ctx.openapi_config_cloud,
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/actor/logs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::*;
use clap::Parser;
use toolchain::errors;
use uuid::Uuid;

#[derive(Parser)]
Expand All @@ -26,7 +27,8 @@ impl Opts {

let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?;

let actor_id = Uuid::parse_str(&self.id).context("invalid id uuid")?;
let actor_id =
Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?;

crate::util::actor::logs::tail(
&ctx,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/build/get.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use anyhow::*;
use clap::Parser;
use toolchain::rivet_api::apis;
use toolchain::{errors, rivet_api::apis};
use uuid::Uuid;

#[derive(Parser)]
Expand All @@ -18,7 +18,8 @@ impl Opts {

let env = crate::util::env::get_or_select(&ctx, self.environment.as_ref()).await?;

let build_id = Uuid::parse_str(&self.id).context("invalid id uuid")?;
let build_id =
Uuid::parse_str(&self.id).map_err(|_| errors::UserError::new("invalid id uuid"))?;

let res = apis::actor_builds_api::actor_builds_get(
&ctx.openapi_config_cloud,
Expand Down
12 changes: 5 additions & 7 deletions packages/cli/src/commands/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use toolchain::tasks;
use crate::util::{
os,
task::{run_task, TaskOutputStyle},
term,
};

/// Login to a project
Expand Down Expand Up @@ -37,10 +36,6 @@ impl Opts {
)
.await?;

// Prompt user to press enter to open browser
println!("Press Enter to login in your browser");
term::wait_for_enter().await?;

// Open link in browser
//
// Linux root users often cannot open the browser, so we fallback to printing the URL
Expand All @@ -52,10 +47,13 @@ impl Opts {
)
.is_ok()
{
println!("Waiting for browser...");
println!(
"Waiting for browser...\n\nIf browser did not open, open this URL to login:\n{}",
device_link_output.device_link_url
);
} else {
println!(
"Failed to open browser.\n\nVisit this URL:\n{}",
"Open this URL to login:\n{}",
device_link_output.device_link_url
);
}
Expand Down
59 changes: 52 additions & 7 deletions packages/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ pub mod util;

use clap::{builder::styling, Parser};
use std::process::ExitCode;

use crate::util::errors;
use toolchain::errors;

const STYLES: styling::Styles = styling::Styles::styled()
.header(styling::AnsiColor::Red.on_default().bold())
Expand Down Expand Up @@ -35,22 +34,68 @@ struct Cli {
command: commands::SubCommand,
}

#[tokio::main]
async fn main() -> ExitCode {
fn main() -> ExitCode {
// We use a sync main for Sentry. Read more: https://docs.sentry.io/platforms/rust/#async-main-function

// This has a 2 second deadline to flush any remaining events which is sufficient for
// short-lived commands.
let _guard = sentry::init(("https://b329eb15c63e1002611fb3b7a58a1dfa@o4504307129188352.ingest.us.sentry.io/4508361147809792", sentry::ClientOptions {
release: sentry::release_name!(),
..Default::default()
}));

// Run main
let exit_code = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move { main_async().await });

exit_code
}

async fn main_async() -> ExitCode {
let cli = Cli::parse();
match cli.command.execute().await {
let exit_code = match cli.command.execute().await {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
if err.is::<errors::GracefulExit>() {
// TODO(TOOL-438): Catch 400 API errors as user errors
if err.is::<errors::GracefulExit>() || err.is::<errors::CtrlC>() {
// Don't print anything, already handled
} else if let Some(err) = err.downcast_ref::<errors::UserError>() {
// Don't report error since this is a user error
eprintln!("{err}");
} else {
// This is an internal error, report error
eprintln!("{err}");
// TODO: Report error
report_error(err).await;
}

ExitCode::FAILURE
}
};

// Wait for telemetry to publish
util::telemetry::wait_all().await;

exit_code
}

async fn report_error(err: anyhow::Error) {
let event_id = sentry::integrations::anyhow::capture_anyhow(&err);

// Capture event in PostHog
let capture_res = util::telemetry::capture_event(
"$exception",
Some(|event: &mut async_posthog::Event| {
event.insert_prop("errors", format!("{}", err))?;
event.insert_prop("$sentry_event_id", event_id.to_string())?;
event.insert_prop("$sentry_url", format!("https://sentry.io/organizations/rivet-gaming/issues/?project=4508361147809792&query={event_id}"))?;
Ok(())
}),
)
.await;
if let Err(err) = capture_res {
eprintln!("Failed to capture event in PostHog: {:?}", err);
}
}
10 changes: 5 additions & 5 deletions packages/cli/src/util/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::*;
use std::collections::HashMap;
use toolchain::tasks::{deploy, get_bootstrap_data};
use uuid::Uuid;

use crate::util::{
use toolchain::{
errors,
task::{run_task, TaskOutputStyle},
tasks::{deploy, get_bootstrap_data},
};
use uuid::Uuid;

use crate::util::task::{run_task, TaskOutputStyle};

pub struct DeployOpts<'a> {
pub environment: &'a str,
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
pub mod actor;
pub mod deploy;
pub mod env;
pub mod errors;
pub mod global_opts;
pub mod os;
pub mod task;
pub mod term;
pub mod telemetry;
137 changes: 137 additions & 0 deletions packages/cli/src/util/telemetry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use anyhow::*;
use serde_json::json;
use sysinfo::System;
use tokio::{
sync::{Mutex, OnceCell},
task::JoinSet,
time::Duration,
};
use toolchain::{meta, paths};

pub static JOIN_SET: OnceCell<Mutex<JoinSet<()>>> = OnceCell::const_new();

/// Get the global join set for telemetry futures.
async fn join_set() -> &'static Mutex<JoinSet<()>> {
JOIN_SET
.get_or_init(|| async { Mutex::new(JoinSet::new()) })
.await
}

/// Waits for all telemetry events to finish.
pub async fn wait_all() {
let mut join_set = join_set().await.lock().await;
match tokio::time::timeout(Duration::from_secs(5), async move {
while join_set.join_next().await.is_some() {}
})
.await
{
Result::Ok(_) => {}
Err(_) => {
println!("Timed out waiting for request to finish")
}
}
}

// This API key is safe to hardcode. It will not change and is intended to be public.
const POSTHOG_API_KEY: &str = "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo";

fn build_client() -> async_posthog::Client {
async_posthog::client(POSTHOG_API_KEY)
}

/// Builds a new PostHog event with associated data.
///
/// This is slightly expensive, so it should not be used frequently.
pub async fn capture_event<F: FnOnce(&mut async_posthog::Event) -> Result<()>>(
name: &str,
mutate: Option<F>,
) -> Result<()> {
// Check if telemetry disabled
let (toolchain_instance_id, telemetry_disabled, api_endpoint) =
meta::read_project(&paths::data_dir()?, |x| {
let api_endpoint = x.cloud.as_ref().map(|cloud| cloud.api_endpoint.clone());
(x.toolchain_instance_id, x.telemetry_disabled, api_endpoint)
})
.await?;

if telemetry_disabled {
return Ok(());
}

// Read project ID. If not signed in or fails to reach server, then ignore.
let (project_id, project_name) = match toolchain::toolchain_ctx::try_load().await {
Result::Ok(Some(ctx)) => (
Some(ctx.project.game_id),
Some(ctx.project.display_name.clone()),
),
Result::Ok(None) => (None, None),
Err(_) => {
// Ignore error
(None, None)
}
};

let distinct_id = format!("toolchain:{toolchain_instance_id}");

let mut event = async_posthog::Event::new(name, &distinct_id);

// Helps us understand what version of the CLI is being used.
let version = json!({
"git_sha": env!("VERGEN_GIT_SHA"),
"git_branch": env!("VERGEN_GIT_BRANCH"),
"build_semver": env!("CARGO_PKG_VERSION"),
"build_timestamp": env!("VERGEN_BUILD_TIMESTAMP"),
"build_target": env!("VERGEN_CARGO_TARGET_TRIPLE"),
"build_debug": env!("VERGEN_CARGO_DEBUG"),
"rustc_version": env!("VERGEN_RUSTC_SEMVER"),
});

// Add properties
if let Some(project_id) = project_id {
event.insert_prop(
"$groups",
&json!({
"project_id": project_id,
}),
)?;
}

event.insert_prop(
"$set",
&json!({
"name": project_name,
"toolchain_instance_id": toolchain_instance_id,
"api_endpoint": api_endpoint,
"version": version,
"project_id": project_id,
"project_root": paths::project_root()?,
"sys": {
"name": System::name(),
"kernel_version": System::kernel_version(),
"os_version": System::os_version(),
"host_name": System::host_name(),
"cpu_arch": System::cpu_arch(),
},
}),
)?;

event.insert_prop("api_endpoint", api_endpoint)?;
event.insert_prop("args", std::env::args().collect::<Vec<_>>())?;

// Customize the event properties
if let Some(mutate) = mutate {
mutate(&mut event)?;
}

// Capture event
join_set().await.lock().await.spawn(async move {
match build_client().capture(event).await {
Result::Ok(_) => {}
Err(_) => {
// Fail silently
}
}
});

Ok(())
}
8 changes: 0 additions & 8 deletions packages/cli/src/util/term.rs

This file was deleted.

Loading
Loading