Skip to content

Commit

Permalink
chore: add sentry integration
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanFlurry committed Nov 25, 2024
1 parent 318adfa commit 7719258
Show file tree
Hide file tree
Showing 17 changed files with 1,004 additions and 62 deletions.
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

0 comments on commit 7719258

Please sign in to comment.