#[cfg(feature = "transport-custom")]
pub mod custom_transport;
pub mod event;
use anyhow::{anyhow, bail, Error, Result};
#[cfg(feature = "transport-custom")]
use custom_transport::Transport;
use event::{Attachment, Event, MinEvent};
use futures_util::{future, FutureExt};
use reqwest::{header::HeaderMap, Client, StatusCode};
use sentry::{Options, Uuid};
use sentry_contrib_native as sentry;
use serde_json::Value;
use std::{
collections::HashMap,
convert::TryInto,
env,
iter::FromIterator,
panic::{self, AssertUnwindSafe},
path::{Path, PathBuf},
process::Stdio,
time::Duration,
};
use tokio::{io::AsyncWriteExt, process::Command, time};
use url::Url;
#[allow(dead_code)]
const NUM_OF_TRIES_SUCCESS: u32 = 20;
#[allow(dead_code)]
const TIME_BETWEEN_TRIES_SUCCESS: Duration = Duration::from_secs(30);
#[allow(dead_code)]
const NUM_OF_TRIES_FAILURE: u32 = 1;
#[allow(dead_code)]
const TIME_BETWEEN_TRIES_FAILURE: Duration = Duration::from_secs(60);
#[derive(Clone)]
struct ApiUrl {
base: Url,
organization_slug: String,
project_slug: String,
}
impl ApiUrl {
async fn new(client: &Client) -> Result<Self> {
let mut api_url = Url::parse(&env::var("SENTRY_DSN")?)?;
let project_id = api_url
.path_segments()
.and_then(|mut path| path.next())
.expect("no projet ID found")
.to_owned();
if let Some(domain) = api_url.domain() {
if domain.ends_with(".ingest.sentry.io") {
api_url.set_host(Some("sentry.io"))?;
}
}
api_url.set_username("").expect("failed to clear username");
api_url
.set_password(None)
.expect("failed to clear username");
api_url
.path_segments_mut()
.expect("failed to clear path")
.clear();
let base = api_url.join("api/")?.join("0/")?;
let (organization_slug, project_slug) = {
let response = client
.get(base.join("projects/")?)
.send()
.await?
.error_for_status()?
.json()
.await?;
Self::slugs(&response, &project_id).expect("couldn't get project or organization slug")
};
Ok(Self {
base,
organization_slug,
project_slug,
})
}
fn slugs(response: &Value, id: &str) -> Option<(String, String)> {
for project in response.as_array()? {
let project = project.as_object()?;
if project.get("id")?.as_str().unwrap() == id {
return Some((
project
.get("organization")?
.as_object()?
.get("slug")?
.as_str()?
.to_owned(),
project.get("slug")?.as_str()?.to_owned(),
));
}
}
None
}
fn event(&self, uuid: Uuid) -> Result<Url> {
self.base
.join("projects/")?
.join(&format!("{}/", self.organization_slug))?
.join(&format!("{}/", self.project_slug))?
.join("events/")?
.join(&format!("{}/", uuid.to_plain()))
.map_err(Into::into)
}
fn attachments(&self, uuid: Uuid) -> Result<Url> {
self.event(uuid)?.join("attachments/").map_err(Into::into)
}
fn issues(&self, user_id: &str) -> Result<Url> {
let mut url = self
.base
.join("projects/")?
.join(&format!("{}/", self.organization_slug))?
.join(&format!("{}/", self.project_slug))?
.join("issues/")?;
url.query_pairs_mut()
.append_pair("query", &format!("user.id:{}", user_id));
url.query_pairs_mut().append_pair("statsPeriod", "24h");
Ok(url)
}
fn events(&self, issue: &str) -> Result<Url> {
self.base
.join("issues/")?
.join(&format!("{}/", issue))?
.join("events/")
.map_err(Into::into)
}
}
async fn init() -> Result<(Client, ApiUrl)> {
let token = env::var("SENTRY_TOKEN")?;
let headers = HeaderMap::from_iter(Some((
"Authorization".try_into()?,
format!("Bearer {}", token).try_into()?,
)));
let client = Client::builder().default_headers(headers).build()?;
let api_url = ApiUrl::new(&client).await?;
Ok((client, api_url))
}
async fn query(
client: &Client,
api_url: Url,
num_of_tries: u32,
time_between_tries: Duration,
) -> Result<Option<Value>> {
for _ in 0..num_of_tries {
let request = client.get(api_url.clone());
time::sleep(time_between_tries).await;
match request.send().await?.error_for_status() {
Ok(response) => return response.json().await.map_err(Into::into),
Err(error) => {
if let Some(error) = error.status() {
match error {
StatusCode::NOT_FOUND | StatusCode::TOO_MANY_REQUESTS => continue,
_ => bail!(error),
}
}
bail!(error)
}
};
}
Ok(None)
}
#[allow(clippy::type_complexity, dead_code)]
pub async fn events_success(
option: Option<fn(&mut Options)>,
events: Vec<(fn() -> Uuid, fn(Event))>,
) -> Result<()> {
let events = events
.into_iter()
.map(|(event, check)| (event, move |event: Option<Event>| check(event.unwrap())))
.collect();
events_internal(
option,
events,
NUM_OF_TRIES_SUCCESS,
TIME_BETWEEN_TRIES_SUCCESS,
)
.await
}
#[allow(dead_code)]
pub async fn events_failure(
option: Option<fn(&mut Options)>,
events: Vec<fn() -> Uuid>,
) -> Result<()> {
let events = events
.into_iter()
.map(|event| (event, move |event: Option<Event>| assert!(event.is_none())))
.collect();
events_internal(
option,
events,
NUM_OF_TRIES_FAILURE,
TIME_BETWEEN_TRIES_FAILURE,
)
.await
}
async fn events_internal(
option: Option<fn(&mut Options)>,
events: Vec<(fn() -> Uuid, impl Fn(Option<Event>) + 'static + Send)>,
num_of_tries: u32,
time_between_tries: Duration,
) -> Result<()> {
let mut options = Options::new();
options.set_debug(true);
options.set_logger(|level, message| eprintln!("[{}]: {}", level, message));
#[cfg(feature = "transport-custom")]
options.set_transport(Transport::new);
if let Some(option) = option {
option(&mut options);
}
let _shutdown = options.init()?;
let mut uuids = Vec::new();
let mut checks = Vec::new();
for (event, check) in events {
uuids.push(event());
checks.push(check);
}
let (client, api_url) = init().await?;
let mut tasks = Vec::new();
for (uuid, check) in uuids.into_iter().zip(checks) {
let client = client.clone();
let api_url = api_url.clone();
tasks.push(
tokio::spawn(async move {
let response =
event(&client, api_url, uuid, num_of_tries, time_between_tries).await?;
let event = response.clone();
panic::catch_unwind(AssertUnwindSafe(|| check(event))).map_err(|error| {
if let Some(event) = response {
eprintln!("Event: {:?}", event)
} else {
eprintln!("[Timeout]: {}", uuid)
}
if let Ok(error) = error.downcast::<Error>() {
*error
} else {
anyhow!("unknown error")
}
})
})
.map(|result| result?),
);
}
future::try_join_all(tasks).await?;
Ok(())
}
async fn event(
client: &Client,
api_url: ApiUrl,
uuid: Uuid,
num_of_tries: u32,
time_between_tries: Duration,
) -> Result<Option<Event>> {
if let Some(response) = query(
client,
api_url.event(uuid)?,
num_of_tries,
time_between_tries,
)
.await?
{
let mut event: Event = serde_json::from_value(response)?;
if let Some(attachments) =
query(client, api_url.attachments(uuid)?, 1, Duration::default()).await?
{
let mut map = HashMap::new();
for attachment in serde_json::from_value::<Vec<Attachment>>(attachments)? {
map.insert(attachment.name.clone(), attachment);
}
event.attachments = map;
return Ok(Some(event));
}
}
Ok(None)
}
#[allow(dead_code)]
pub async fn external_events_success(events: Vec<(String, fn(Event))>) -> Result<()> {
let events = events
.into_iter()
.map(|(event, check)| (event, move |event: Option<Event>| check(event.unwrap())))
.collect();
external_events_internal(events, NUM_OF_TRIES_SUCCESS, TIME_BETWEEN_TRIES_SUCCESS).await
}
#[allow(dead_code)]
pub async fn external_events_failure(events: Vec<String>) -> Result<()> {
let events = events
.into_iter()
.map(|event| (event, move |event: Option<Event>| assert!(event.is_none())))
.collect();
external_events_internal(events, NUM_OF_TRIES_FAILURE, TIME_BETWEEN_TRIES_FAILURE).await
}
async fn external_events_internal(
events: Vec<(String, impl Fn(Option<Event>) + 'static + Send)>,
num_of_tries: u32,
time_between_tries: Duration,
) -> Result<()> {
let (client, api_url) = init().await?;
let example_path = PathBuf::from(env!("OUT_DIR"))
.parent()
.and_then(Path::parent)
.and_then(Path::parent)
.unwrap()
.join("examples");
let mut tasks = Vec::new();
for (example, check) in events {
let client = client.clone();
let api_url = api_url.clone();
#[cfg(not(target_os = "windows"))]
let example = example_path.join(example);
#[cfg(target_os = "windows")]
let example = example_path.join(format!("{}.exe", example));
tasks.push(
tokio::spawn(async move {
let id: [u8; 16] = rand::random();
let user_id = hex::encode(id);
let mut child = Command::new(example)
.stdin(Stdio::piped())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.expect("make sure to build the example first!");
child.stdin.as_mut().unwrap().write_all(&id).await?;
assert!(!child.wait().await?.success());
let event = event_by_user(
&client,
api_url,
user_id.clone(),
num_of_tries,
time_between_tries,
)
.await?;
panic::catch_unwind(AssertUnwindSafe(|| check(event.clone()))).map_err(|error| {
if let Some(event) = event {
eprintln!("Event: {:?}", event)
} else {
eprintln!("[Timeout]: {}", user_id)
}
if let Ok(error) = error.downcast::<Error>() {
*error
} else {
anyhow!("unknown error")
}
})
})
.map(|result| result?),
);
}
future::try_join_all(tasks).await?;
Ok(())
}
#[allow(dead_code)]
async fn event_by_user(
client: &Client,
api_url: ApiUrl,
user_id: String,
num_of_tries: u32,
time_between_tries: Duration,
) -> Result<Option<Event>> {
let mut issues = None;
for _ in 0..num_of_tries {
if let Some(Value::Array(value)) =
query(client, api_url.issues(&user_id)?, 1, time_between_tries).await?
{
if value.is_empty() {
continue;
}
issues = Some(value);
break;
}
}
let issues = match issues {
None => return Ok(None),
Some(issues) => issues,
};
let issue = issues[0]
.as_object()
.unwrap()
.get("id")
.unwrap()
.as_str()
.unwrap();
let events: Vec<MinEvent> = serde_json::from_value(
query(
client,
api_url.events(issue)?,
NUM_OF_TRIES_SUCCESS,
TIME_BETWEEN_TRIES_SUCCESS,
)
.await?
.unwrap(),
)?;
for event in events {
if let Some(user) = event.user {
if let Some(id) = user.id {
if id == user_id {
let uuid: [u8; 16] = hex::decode(event.event_id)?.as_slice().try_into()?;
let uuid = Uuid::from(uuid);
return self::event(client, api_url.clone(), uuid, 1, Duration::default())
.await;
}
}
}
}
Ok(None)
}