1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#![doc(
    html_logo_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png",
    html_favicon_url = "https://raw.githubusercontent.com/getsentry/relay/master/artwork/relay-icon.png"
)]

use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;

use anyhow::{format_err, Context, Result};
use clap::Parser;
use relay_event_normalization::{
    normalize_event, validate_event_timestamps, validate_transaction, EventValidationConfig,
    NormalizationConfig, TransactionValidationConfig,
};
use relay_event_schema::processor::{process_value, ProcessingState};
use relay_event_schema::protocol::Event;
use relay_pii::{PiiConfig, PiiProcessor};
use relay_protocol::Annotated;

/// Processes a Sentry event payload.
///
/// This command takes a JSON event payload on stdin and write the processed event payload to
/// stdout. Optionally, an additional PII config can be supplied.
#[derive(Debug, Parser)]
#[structopt(verbatim_doc_comment)]
struct Cli {
    /// Path to a PII processing config JSON file.
    #[arg(short = 'c', long)]
    pii_config: Option<PathBuf>,

    /// Path to an event payload JSON file (defaults to stdin).
    #[arg(short, long)]
    event: Option<PathBuf>,

    /// Apply full store normalization.
    #[arg(long)]
    store: bool,

    /// Pretty print the output JSON.
    #[arg(long, conflicts_with = "debug")]
    pretty: bool,

    /// Debug print the internal structure.
    #[arg(long)]
    debug: bool,
}

impl Cli {
    fn load_pii_config(&self) -> Result<Option<PiiConfig>> {
        let path = match self.pii_config {
            Some(ref path) => path,
            None => return Ok(None),
        };

        let json = fs::read_to_string(path).with_context(|| "failed to read PII config")?;
        let config = serde_json::from_str(&json).with_context(|| "failed to parse PII config")?;
        Ok(Some(config))
    }

    fn load_event(&self) -> Result<Annotated<Event>> {
        let json = match self.event {
            Some(ref path) => fs::read_to_string(path).with_context(|| "failed to read event")?,
            None => {
                let mut json = String::new();
                io::stdin()
                    .read_to_string(&mut json)
                    .with_context(|| "failed to read event")?;
                json
            }
        };

        let event = Annotated::from_json(&json).with_context(|| "failed to parse event")?;
        Ok(event)
    }

    pub fn run(self) -> Result<()> {
        let mut event = self.load_event()?;

        if let Some(pii_config) = self.load_pii_config()? {
            let mut processor = PiiProcessor::new(pii_config.compiled());
            process_value(&mut event, &mut processor, ProcessingState::root())
                .map_err(|e| format_err!("{e}"))?;
        }

        if self.store {
            validate_event_timestamps(&mut event, &EventValidationConfig::default())
                .map_err(|e| format_err!("{e}"))?;
            validate_transaction(&mut event, &TransactionValidationConfig::default())
                .map_err(|e| format_err!("{e}"))?;
            normalize_event(&mut event, &NormalizationConfig::default());
        }

        if self.debug {
            println!("{event:#?}");
        } else if self.pretty {
            println!("{}", event.to_json_pretty()?);
        } else {
            println!("{}", event.to_json()?);
        }

        Ok(())
    }
}

fn print_error(error: &anyhow::Error) {
    eprintln!("Error: {error}");

    let mut cause = error.source();
    while let Some(ref e) = cause {
        eprintln!("  caused by: {e}");
        cause = e.source();
    }
}

fn main() {
    let cli = Cli::parse();

    match cli.run() {
        Ok(()) => (),
        Err(error) => {
            print_error(&error);
            std::process::exit(1);
        }
    }
}