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

Commit d7a6508

Browse files
committedDec 1, 2024
chore: add sentry integration (#592)
Fixes RVT-4143
1 parent 8126ac2 commit d7a6508

File tree

17 files changed

+1004
-62
lines changed

17 files changed

+1004
-62
lines changed
 

‎Cargo.lock

+727-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/cli/Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ repository = "https://github.com/rivet-gg/cli"
1010
name = "rivet"
1111
path = "src/main.rs"
1212

13+
[features]
14+
default = ["sentry"]
15+
sentry = []
16+
1317
[dependencies]
1418
clap = { version = "4.5.9", features = ["derive"] }
1519
toolchain = { version = "0.1.0", path = "../toolchain", package = "rivet-toolchain" }
@@ -23,6 +27,13 @@ base64 = "0.22.1"
2327
kv-str = { version = "0.1.0", path = "../kv-str" }
2428
inquire = "0.7.5"
2529
webbrowser = "1.0.2"
30+
sentry = { version = "0.34.0", features = ["anyhow"] }
31+
sysinfo = "0.32.0"
32+
ctrlc = "3.4.5"
33+
34+
[dependencies.async-posthog]
35+
git = "https://github.com/rivet-gg/posthog-rs"
36+
rev = "ef4e80e"
2637

2738
[build-dependencies]
2839
anyhow = "1.0"

‎packages/cli/src/commands/actor/create.rs

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use toolchain::{
88
};
99
use uuid::Uuid;
1010

11-
1211
#[derive(ValueEnum, Clone)]
1312
enum NetworkMode {
1413
Bridge,

‎packages/cli/src/commands/actor/destroy.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::*;
22
use clap::Parser;
3-
use toolchain::rivet_api::apis;
3+
use toolchain::{errors, rivet_api::apis};
44
use uuid::Uuid;
55

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

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

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

2627
apis::actor_api::actor_destroy(
2728
&ctx.openapi_config_cloud,

‎packages/cli/src/commands/actor/logs.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use anyhow::*;
22
use clap::Parser;
3+
use toolchain::errors;
34
use uuid::Uuid;
45

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

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

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

3133
crate::util::actor::logs::tail(
3234
&ctx,

‎packages/cli/src/commands/build/get.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::*;
22
use clap::Parser;
3-
use toolchain::rivet_api::apis;
3+
use toolchain::{errors, rivet_api::apis};
44
use uuid::Uuid;
55

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

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

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

2324
let res = apis::actor_builds_api::actor_builds_get(
2425
&ctx.openapi_config_cloud,

‎packages/cli/src/commands/login.rs

+5-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ use toolchain::tasks;
55
use crate::util::{
66
os,
77
task::{run_task, TaskOutputStyle},
8-
term,
98
};
109

1110
/// Login to a project
@@ -37,10 +36,6 @@ impl Opts {
3736
)
3837
.await?;
3938

40-
// Prompt user to press enter to open browser
41-
println!("Press Enter to login in your browser");
42-
term::wait_for_enter().await?;
43-
4439
// Open link in browser
4540
//
4641
// Linux root users often cannot open the browser, so we fallback to printing the URL
@@ -52,10 +47,13 @@ impl Opts {
5247
)
5348
.is_ok()
5449
{
55-
println!("Waiting for browser...");
50+
println!(
51+
"Waiting for browser...\n\nIf browser did not open, open this URL to login:\n{}",
52+
device_link_output.device_link_url
53+
);
5654
} else {
5755
println!(
58-
"Failed to open browser.\n\nVisit this URL:\n{}",
56+
"Open this URL to login:\n{}",
5957
device_link_output.device_link_url
6058
);
6159
}

‎packages/cli/src/main.rs

+52-7
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ pub mod util;
33

44
use clap::{builder::styling, Parser};
55
use std::process::ExitCode;
6-
7-
use crate::util::errors;
6+
use toolchain::errors;
87

98
const STYLES: styling::Styles = styling::Styles::styled()
109
.header(styling::AnsiColor::Red.on_default().bold())
@@ -35,22 +34,68 @@ struct Cli {
3534
command: commands::SubCommand,
3635
}
3736

38-
#[tokio::main]
39-
async fn main() -> ExitCode {
37+
fn main() -> ExitCode {
38+
// We use a sync main for Sentry. Read more: https://docs.sentry.io/platforms/rust/#async-main-function
39+
40+
// This has a 2 second deadline to flush any remaining events which is sufficient for
41+
// short-lived commands.
42+
let _guard = sentry::init(("https://b329eb15c63e1002611fb3b7a58a1dfa@o4504307129188352.ingest.us.sentry.io/4508361147809792", sentry::ClientOptions {
43+
release: sentry::release_name!(),
44+
..Default::default()
45+
}));
46+
47+
// Run main
48+
let exit_code = tokio::runtime::Builder::new_multi_thread()
49+
.enable_all()
50+
.build()
51+
.unwrap()
52+
.block_on(async move { main_async().await });
53+
54+
exit_code
55+
}
56+
57+
async fn main_async() -> ExitCode {
4058
let cli = Cli::parse();
41-
match cli.command.execute().await {
59+
let exit_code = match cli.command.execute().await {
4260
Ok(()) => ExitCode::SUCCESS,
4361
Err(err) => {
44-
if err.is::<errors::GracefulExit>() {
62+
// TODO(TOOL-438): Catch 400 API errors as user errors
63+
if err.is::<errors::GracefulExit>() || err.is::<errors::CtrlC>() {
4564
// Don't print anything, already handled
4665
} else if let Some(err) = err.downcast_ref::<errors::UserError>() {
66+
// Don't report error since this is a user error
4767
eprintln!("{err}");
4868
} else {
69+
// This is an internal error, report error
4970
eprintln!("{err}");
50-
// TODO: Report error
71+
report_error(err).await;
5172
}
5273

5374
ExitCode::FAILURE
5475
}
76+
};
77+
78+
// Wait for telemetry to publish
79+
util::telemetry::wait_all().await;
80+
81+
exit_code
82+
}
83+
84+
async fn report_error(err: anyhow::Error) {
85+
let event_id = sentry::integrations::anyhow::capture_anyhow(&err);
86+
87+
// Capture event in PostHog
88+
let capture_res = util::telemetry::capture_event(
89+
"$exception",
90+
Some(|event: &mut async_posthog::Event| {
91+
event.insert_prop("errors", format!("{}", err))?;
92+
event.insert_prop("$sentry_event_id", event_id.to_string())?;
93+
event.insert_prop("$sentry_url", format!("https://sentry.io/organizations/rivet-gaming/issues/?project=4508361147809792&query={event_id}"))?;
94+
Ok(())
95+
}),
96+
)
97+
.await;
98+
if let Err(err) = capture_res {
99+
eprintln!("Failed to capture event in PostHog: {:?}", err);
55100
}
56101
}

‎packages/cli/src/util/deploy.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use anyhow::*;
22
use std::collections::HashMap;
3-
use toolchain::tasks::{deploy, get_bootstrap_data};
4-
use uuid::Uuid;
5-
6-
use crate::util::{
3+
use toolchain::{
74
errors,
8-
task::{run_task, TaskOutputStyle},
5+
tasks::{deploy, get_bootstrap_data},
96
};
7+
use uuid::Uuid;
8+
9+
use crate::util::task::{run_task, TaskOutputStyle};
1010

1111
pub struct DeployOpts<'a> {
1212
pub environment: &'a str,

‎packages/cli/src/util/mod.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
pub mod actor;
22
pub mod deploy;
33
pub mod env;
4-
pub mod errors;
54
pub mod global_opts;
65
pub mod os;
76
pub mod task;
8-
pub mod term;
7+
pub mod telemetry;

‎packages/cli/src/util/telemetry.rs

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use anyhow::*;
2+
use serde_json::json;
3+
use sysinfo::System;
4+
use tokio::{
5+
sync::{Mutex, OnceCell},
6+
task::JoinSet,
7+
time::Duration,
8+
};
9+
use toolchain::{meta, paths};
10+
11+
pub static JOIN_SET: OnceCell<Mutex<JoinSet<()>>> = OnceCell::const_new();
12+
13+
/// Get the global join set for telemetry futures.
14+
async fn join_set() -> &'static Mutex<JoinSet<()>> {
15+
JOIN_SET
16+
.get_or_init(|| async { Mutex::new(JoinSet::new()) })
17+
.await
18+
}
19+
20+
/// Waits for all telemetry events to finish.
21+
pub async fn wait_all() {
22+
let mut join_set = join_set().await.lock().await;
23+
match tokio::time::timeout(Duration::from_secs(5), async move {
24+
while join_set.join_next().await.is_some() {}
25+
})
26+
.await
27+
{
28+
Result::Ok(_) => {}
29+
Err(_) => {
30+
println!("Timed out waiting for request to finish")
31+
}
32+
}
33+
}
34+
35+
// This API key is safe to hardcode. It will not change and is intended to be public.
36+
const POSTHOG_API_KEY: &str = "phc_6kfTNEAVw7rn1LA51cO3D69FefbKupSWFaM7OUgEpEo";
37+
38+
fn build_client() -> async_posthog::Client {
39+
async_posthog::client(POSTHOG_API_KEY)
40+
}
41+
42+
/// Builds a new PostHog event with associated data.
43+
///
44+
/// This is slightly expensive, so it should not be used frequently.
45+
pub async fn capture_event<F: FnOnce(&mut async_posthog::Event) -> Result<()>>(
46+
name: &str,
47+
mutate: Option<F>,
48+
) -> Result<()> {
49+
// Check if telemetry disabled
50+
let (toolchain_instance_id, telemetry_disabled, api_endpoint) =
51+
meta::read_project(&paths::data_dir()?, |x| {
52+
let api_endpoint = x.cloud.as_ref().map(|cloud| cloud.api_endpoint.clone());
53+
(x.toolchain_instance_id, x.telemetry_disabled, api_endpoint)
54+
})
55+
.await?;
56+
57+
if telemetry_disabled {
58+
return Ok(());
59+
}
60+
61+
// Read project ID. If not signed in or fails to reach server, then ignore.
62+
let (project_id, project_name) = match toolchain::toolchain_ctx::try_load().await {
63+
Result::Ok(Some(ctx)) => (
64+
Some(ctx.project.game_id),
65+
Some(ctx.project.display_name.clone()),
66+
),
67+
Result::Ok(None) => (None, None),
68+
Err(_) => {
69+
// Ignore error
70+
(None, None)
71+
}
72+
};
73+
74+
let distinct_id = format!("toolchain:{toolchain_instance_id}");
75+
76+
let mut event = async_posthog::Event::new(name, &distinct_id);
77+
78+
// Helps us understand what version of the CLI is being used.
79+
let version = json!({
80+
"git_sha": env!("VERGEN_GIT_SHA"),
81+
"git_branch": env!("VERGEN_GIT_BRANCH"),
82+
"build_semver": env!("CARGO_PKG_VERSION"),
83+
"build_timestamp": env!("VERGEN_BUILD_TIMESTAMP"),
84+
"build_target": env!("VERGEN_CARGO_TARGET_TRIPLE"),
85+
"build_debug": env!("VERGEN_CARGO_DEBUG"),
86+
"rustc_version": env!("VERGEN_RUSTC_SEMVER"),
87+
});
88+
89+
// Add properties
90+
if let Some(project_id) = project_id {
91+
event.insert_prop(
92+
"$groups",
93+
&json!({
94+
"project_id": project_id,
95+
}),
96+
)?;
97+
}
98+
99+
event.insert_prop(
100+
"$set",
101+
&json!({
102+
"name": project_name,
103+
"toolchain_instance_id": toolchain_instance_id,
104+
"api_endpoint": api_endpoint,
105+
"version": version,
106+
"project_id": project_id,
107+
"project_root": paths::project_root()?,
108+
"sys": {
109+
"name": System::name(),
110+
"kernel_version": System::kernel_version(),
111+
"os_version": System::os_version(),
112+
"host_name": System::host_name(),
113+
"cpu_arch": System::cpu_arch(),
114+
},
115+
}),
116+
)?;
117+
118+
event.insert_prop("api_endpoint", api_endpoint)?;
119+
event.insert_prop("args", std::env::args().collect::<Vec<_>>())?;
120+
121+
// Customize the event properties
122+
if let Some(mutate) = mutate {
123+
mutate(&mut event)?;
124+
}
125+
126+
// Capture event
127+
join_set().await.lock().await.spawn(async move {
128+
match build_client().capture(event).await {
129+
Result::Ok(_) => {}
130+
Err(_) => {
131+
// Fail silently
132+
}
133+
}
134+
});
135+
136+
Ok(())
137+
}

‎packages/cli/src/util/term.rs

-8
This file was deleted.

‎packages/cli/src/util/errors.rs ‎packages/toolchain/src/errors.rs

+19
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,25 @@ impl std::fmt::Display for GracefulExit {
1818

1919
impl std::error::Error for GracefulExit {}
2020

21+
/// This error type will exit without printing anything.
22+
///
23+
/// This indicates the program was exited with a Ctrl-C event.
24+
pub struct CtrlC;
25+
26+
impl std::fmt::Debug for CtrlC {
27+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28+
f.debug_struct("CtrlC").finish()
29+
}
30+
}
31+
32+
impl std::fmt::Display for CtrlC {
33+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34+
write!(f, "CtrlC occurred")
35+
}
36+
}
37+
38+
impl std::error::Error for CtrlC {}
39+
2140
/// This error type will exit with a message, but will not report the error to Rivet.
2241
///
2342
/// This should be used for errors where the user input is incorrect.

‎packages/toolchain/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod build;
22
pub mod config;
3+
pub mod errors;
34
pub mod meta;
45
pub mod paths;
56
pub mod project;

‎packages/toolchain/src/meta.rs

+23-6
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,47 @@ use std::{collections::HashMap, path::PathBuf};
55
use tokio::{fs, sync::Mutex};
66
use uuid::Uuid;
77

8-
use crate::paths;
8+
use crate::{errors, paths};
99

1010
/// Config stored in {data_dir}/meta.json. Used to store persistent data, such as tokens & cache.
11-
#[derive(Default, Serialize, Deserialize)]
11+
#[derive(Serialize, Deserialize)]
1212
pub struct Meta {
13+
/// Unique ID for this instance of the toolchain.
14+
///
15+
/// This ID is unique to each project folder.
16+
pub toolchain_instance_id: Uuid,
17+
1318
/// If logged in to Rivet, this will include relevant information.
1419
///
1520
/// If not logged in, will be None.
1621
pub cloud: Option<Cloud>,
22+
23+
pub telemetry_disabled: bool,
24+
}
25+
26+
impl Meta {
27+
fn new() -> Self {
28+
Self {
29+
toolchain_instance_id: Uuid::new_v4(),
30+
cloud: None,
31+
telemetry_disabled: false,
32+
}
33+
}
1734
}
1835

1936
impl Meta {
2037
pub fn cloud(&self) -> Result<&Cloud> {
2138
Ok(self
2239
.cloud
2340
.as_ref()
24-
.context("Not logged in. Please run `rivet login`.")?)
41+
.ok_or_else(|| errors::UserError::new("Not logged in. Please run `rivet login`."))?)
2542
}
2643

2744
pub fn cloud_mut(&mut self) -> Result<&mut Cloud> {
2845
Ok(self
2946
.cloud
3047
.as_mut()
31-
.context("Not logged in. Please run `rivet login`.")?)
48+
.ok_or_else(|| errors::UserError::new("Not logged in. Please run `rivet login`."))?)
3249
}
3350
}
3451

@@ -96,7 +113,7 @@ pub async fn try_read_project<F: FnOnce(&Meta) -> Result<T>, T>(
96113
let mut meta = match fs::read_to_string(&meta_path).await {
97114
Result::Ok(config) => serde_json::from_str::<Meta>(&config)
98115
.context(format!("deserialize meta ({})", meta_path.display()))?,
99-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::default(),
116+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::new(),
100117
Err(err) => return Err(err.into()),
101118
};
102119

@@ -131,7 +148,7 @@ pub async fn try_mutate_project<F: FnOnce(&mut Meta) -> Result<T>, T>(
131148
let mut meta = match fs::read_to_string(&meta_path).await {
132149
Result::Ok(config) => serde_json::from_str::<Meta>(&config)
133150
.context(format!("deserialize meta ({})", meta_path.display()))?,
134-
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::default(),
151+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Meta::new(),
135152
Err(err) => return Err(err.into()),
136153
};
137154

‎packages/toolchain/src/toolchain_ctx.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::*;
22
use pkg_version::{pkg_version_major, pkg_version_minor, pkg_version_patch};
33
use rivet_api::{apis, models};
44
use std::{env, sync::Arc};
5+
use tokio::sync::OnceCell;
56

67
use crate::{meta, paths};
78

@@ -30,6 +31,8 @@ pub struct CtxInner {
3031
pub openapi_config_cloud: apis::configuration::Configuration,
3132
}
3233

34+
static TOOLCHAIN_CTX: OnceCell<ToolchainCtx> = OnceCell::const_new();
35+
3336
pub async fn try_load() -> Result<Option<ToolchainCtx>> {
3437
let data = meta::read_project(&paths::data_dir()?, |x| {
3538
x.cloud
@@ -38,8 +41,10 @@ pub async fn try_load() -> Result<Option<ToolchainCtx>> {
3841
})
3942
.await?;
4043
if let Some((api_endpoint, token)) = data {
41-
let ctx = init(api_endpoint, token).await?;
42-
Ok(Some(ctx))
44+
let ctx = TOOLCHAIN_CTX
45+
.get_or_try_init(|| async { init(api_endpoint, token).await })
46+
.await?;
47+
Ok(Some(ctx.clone()))
4348
} else {
4449
Ok(None)
4550
}
@@ -51,7 +56,10 @@ pub async fn load() -> Result<ToolchainCtx> {
5156
Ok((cloud.api_endpoint.clone(), cloud.cloud_token.clone()))
5257
})
5358
.await?;
54-
init(api_endpoint, token).await
59+
let ctx = TOOLCHAIN_CTX
60+
.get_or_try_init(|| async { init(api_endpoint, token).await })
61+
.await?;
62+
Ok(ctx.clone())
5563
}
5664

5765
pub async fn init(api_endpoint: String, cloud_token: String) -> Result<ToolchainCtx> {

‎packages/toolchain/src/util/task/run.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use tokio::sync::{broadcast, mpsc};
33

44
use super::{Task, TaskCtxInner, TaskEvent};
55

6+
use crate::errors;
7+
68
/// Run config passed to the task.
79
pub struct RunConfig {
810
pub abort_rx: mpsc::Receiver<()>,
@@ -71,7 +73,7 @@ where
7173
// Shutdown
7274
shutdown_tx.send(())?;
7375

74-
Err(anyhow!("task aborted"))
76+
Err(errors::GracefulExit.into())
7577
},
7678
}
7779
}

0 commit comments

Comments
 (0)
This repository has been archived.