relay_cabi/
processing.rs

1// TODO: Fix casts between RelayGeoIpLookup and GeoIpLookup
2#![allow(clippy::cast_ptr_alignment)]
3#![deny(unused_must_use)]
4#![allow(clippy::derive_partial_eq_without_eq)]
5
6use std::cmp::Ordering;
7use std::ffi::CStr;
8use std::os::raw::c_char;
9use std::slice;
10use std::sync::OnceLock;
11
12use chrono::{DateTime, Utc};
13use relay_cardinality::CardinalityLimit;
14use relay_dynamic_config::{GlobalConfig, ProjectConfig, normalize_json};
15use relay_event_normalization::{
16    BreakdownsConfig, ClientHints, EventValidationConfig, GeoIpLookup, NormalizationConfig,
17    RawUserAgentInfo, normalize_event, validate_event,
18};
19use relay_event_schema::processor::{ProcessingState, process_value, split_chunks};
20use relay_event_schema::protocol::{Event, IpAddr, VALID_PLATFORMS};
21use relay_pii::{
22    DataScrubbingConfig, InvalidSelectorError, PiiConfig, PiiConfigError, PiiProcessor,
23    SelectorSpec, selector_suggestions_from_value,
24};
25use relay_protocol::{Annotated, Remark, RuleCondition};
26use relay_sampling::SamplingConfig;
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30use crate::core::RelayStr;
31
32/// Configuration for the store step -- validation and normalization.
33#[derive(Serialize, Deserialize, Debug, Default)]
34#[serde(default)]
35pub struct StoreNormalizer {
36    /// The identifier of the target project, which gets added to the payload.
37    pub project_id: Option<u64>,
38
39    /// The IP address of the SDK that sent the event.
40    ///
41    /// When `{{auto}}` is specified and there is no other IP address in the payload, such as in the
42    /// `request` context, this IP address gets added to the `user` context.
43    pub client_ip: Option<IpAddr>,
44
45    /// The name and version of the SDK that sent the event.
46    pub client: Option<String>,
47
48    /// The internal identifier of the DSN, which gets added to the payload.
49    ///
50    /// Note that this is different from the DSN's public key. The ID is usually numeric.
51    pub key_id: Option<String>,
52
53    /// The version of the protocol.
54    ///
55    /// This is a deprecated field, as there is no more versioning of Relay event payloads.
56    pub protocol_version: Option<String>,
57
58    /// Configuration for issue grouping.
59    ///
60    /// This configuration is persisted into the event payload to achieve idempotency in the
61    /// processing pipeline and for reprocessing.
62    pub grouping_config: Option<serde_json::Value>,
63
64    /// The raw user-agent string obtained from the submission request headers.
65    ///
66    /// The user agent is used to infer device, operating system, and browser information should the
67    /// event payload contain no such data.
68    ///
69    /// Newer browsers have frozen their user agents and send [`client_hints`](Self::client_hints)
70    /// instead. If both a user agent and client hints are present, normalization uses client hints.
71    pub user_agent: Option<String>,
72
73    /// A collection of headers sent by newer browsers about the device and environment.
74    ///
75    /// Client hints are the preferred way to infer device, operating system, and browser
76    /// information should the event payload contain no such data. If no client hints are present,
77    /// normalization falls back to the user agent.
78    pub client_hints: ClientHints<String>,
79
80    /// The time at which the event was received in this Relay.
81    ///
82    /// This timestamp is persisted into the event payload.
83    pub received_at: Option<DateTime<Utc>>,
84
85    /// The time at which the event was sent by the client.
86    ///
87    /// The difference between this and the `received_at` timestamps is used for clock drift
88    /// correction, should a significant difference be detected.
89    pub sent_at: Option<DateTime<Utc>>,
90
91    /// The maximum amount of seconds an event can be predated into the future.
92    ///
93    /// If the event's timestamp lies further into the future, the received timestamp is assumed.
94    pub max_secs_in_future: Option<i64>,
95
96    /// The maximum amount of seconds an event can be dated in the past.
97    ///
98    /// If the event's timestamp is older, the received timestamp is assumed.
99    pub max_secs_in_past: Option<i64>,
100
101    /// When `Some(true)`, individual parts of the event payload is trimmed to a maximum size.
102    ///
103    /// See the event schema for size declarations.
104    pub enable_trimming: Option<bool>,
105
106    /// When `Some(true)`, it is assumed that the event has been normalized before.
107    ///
108    /// This disables certain normalizations, especially all that are not idempotent. The
109    /// renormalize mode is intended for the use in the processing pipeline, so an event modified
110    /// during ingestion can be validated against the schema and large data can be trimmed. However,
111    /// advanced normalizations such as inferring contexts or clock drift correction are disabled.
112    ///
113    /// `None` equals to `false`.
114    pub is_renormalize: Option<bool>,
115
116    /// Overrides the default flag for other removal.
117    pub remove_other: Option<bool>,
118
119    /// When `Some(true)`, context information is extracted from the user agent.
120    pub normalize_user_agent: Option<bool>,
121
122    /// Emit breakdowns based on given configuration.
123    pub breakdowns: Option<BreakdownsConfig>,
124
125    /// The SDK's sample rate as communicated via envelope headers.
126    ///
127    /// It is persisted into the event payload.
128    pub client_sample_rate: Option<f64>,
129
130    /// The identifier of the Replay running while this event was created.
131    ///
132    /// It is persisted into the event payload for correlation.
133    pub replay_id: Option<Uuid>,
134
135    /// Controls whether spans should be normalized (e.g. normalizing the exclusive time).
136    ///
137    /// To normalize spans in [`normalize_event`], `is_renormalize` must
138    /// be disabled _and_ `normalize_spans` enabled.
139    pub normalize_spans: bool,
140}
141
142impl StoreNormalizer {
143    /// Helper method to parse *mut StoreConfig -> &StoreConfig
144    fn this(&self) -> &Self {
145        self
146    }
147}
148
149/// A geo ip lookup helper based on maxmind db files.
150pub struct RelayGeoIpLookup;
151
152/// The processor that normalizes events for store.
153pub struct RelayStoreNormalizer;
154
155/// Chunks the given text based on remarks.
156#[unsafe(no_mangle)]
157#[relay_ffi::catch_unwind]
158pub unsafe extern "C" fn relay_split_chunks(
159    string: *const RelayStr,
160    remarks: *const RelayStr,
161) -> RelayStr {
162    let remarks: Vec<Remark> = serde_json::from_str(unsafe { (*remarks).as_str() })?;
163    let chunks = split_chunks(unsafe { (*string).as_str() }, &remarks);
164    let json = serde_json::to_string(&chunks)?;
165    json.into()
166}
167
168/// Opens a maxminddb file by path.
169#[unsafe(no_mangle)]
170#[relay_ffi::catch_unwind]
171pub unsafe extern "C" fn relay_geoip_lookup_new(path: *const c_char) -> *mut RelayGeoIpLookup {
172    let path = unsafe { CStr::from_ptr(path) }.to_string_lossy();
173    let lookup = GeoIpLookup::open(path.as_ref())?;
174    Box::into_raw(Box::new(lookup)) as *mut RelayGeoIpLookup
175}
176
177/// Frees a `RelayGeoIpLookup`.
178#[unsafe(no_mangle)]
179#[relay_ffi::catch_unwind]
180pub unsafe extern "C" fn relay_geoip_lookup_free(lookup: *mut RelayGeoIpLookup) {
181    if !lookup.is_null() {
182        let lookup = lookup as *mut GeoIpLookup;
183        let _dropped = unsafe { Box::from_raw(lookup) };
184    }
185}
186
187/// Returns a list of all valid platform identifiers.
188#[unsafe(no_mangle)]
189#[relay_ffi::catch_unwind]
190pub unsafe extern "C" fn relay_valid_platforms(size_out: *mut usize) -> *const RelayStr {
191    static VALID_PLATFORM_STRS: OnceLock<Vec<RelayStr>> = OnceLock::new();
192    let platforms = VALID_PLATFORM_STRS
193        .get_or_init(|| VALID_PLATFORMS.iter().map(|s| RelayStr::new(s)).collect());
194
195    if let Some(size_out) = unsafe { size_out.as_mut() } {
196        *size_out = platforms.len();
197    }
198
199    platforms.as_ptr()
200}
201
202/// Creates a new normalization config.
203#[unsafe(no_mangle)]
204#[relay_ffi::catch_unwind]
205pub unsafe extern "C" fn relay_store_normalizer_new(
206    config: *const RelayStr,
207    _geoip_lookup: *const RelayGeoIpLookup,
208) -> *mut RelayStoreNormalizer {
209    let normalizer: StoreNormalizer = serde_json::from_str(unsafe { (*config).as_str() })?;
210    Box::into_raw(Box::new(normalizer)) as *mut RelayStoreNormalizer
211}
212
213/// Frees a `RelayStoreNormalizer`.
214#[unsafe(no_mangle)]
215#[relay_ffi::catch_unwind]
216pub unsafe extern "C" fn relay_store_normalizer_free(normalizer: *mut RelayStoreNormalizer) {
217    if !normalizer.is_null() {
218        let normalizer = normalizer as *mut StoreNormalizer;
219        let _dropped = unsafe { Box::from_raw(normalizer) };
220    }
221}
222
223/// Normalizes the event given as JSON.
224#[unsafe(no_mangle)]
225#[relay_ffi::catch_unwind]
226pub unsafe extern "C" fn relay_store_normalizer_normalize_event(
227    normalizer: *mut RelayStoreNormalizer,
228    event: *const RelayStr,
229) -> RelayStr {
230    let normalizer = normalizer as *mut StoreNormalizer;
231    let config = unsafe { (*normalizer).this() };
232    let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
233
234    let event_validation_config = EventValidationConfig {
235        received_at: config.received_at,
236        max_secs_in_past: config.max_secs_in_past,
237        max_secs_in_future: config.max_secs_in_future,
238        transaction_timestamp_range: None, // only supported in relay
239        is_validated: config.is_renormalize.unwrap_or(false),
240    };
241    validate_event(&mut event, &event_validation_config)?;
242
243    let is_renormalize = config.is_renormalize.unwrap_or(false);
244
245    let normalization_config = NormalizationConfig {
246        project_id: config.project_id,
247        client: config.client.clone(),
248        protocol_version: config.protocol_version.clone(),
249        key_id: config.key_id.clone(),
250        grouping_config: config.grouping_config.clone(),
251        client_ip: config.client_ip.as_ref(),
252        infer_ip_address: false, // only supported in relay
253        client_sample_rate: config.client_sample_rate,
254        user_agent: RawUserAgentInfo {
255            user_agent: config.user_agent.as_deref(),
256            client_hints: config.client_hints.as_deref(),
257        },
258        max_name_and_unit_len: None,
259        breakdowns_config: None, // only supported in relay
260        normalize_user_agent: config.normalize_user_agent,
261        transaction_name_config: Default::default(), // only supported in relay
262        is_renormalize,
263        remove_other: config.remove_other.unwrap_or(!is_renormalize),
264        emit_event_errors: !is_renormalize,
265        device_class_synthesis_config: false, // only supported in relay
266        enrich_spans: false,
267        max_tag_value_length: usize::MAX,
268        span_description_rules: None,
269        performance_score: None,
270        geoip_lookup: None,          // only supported in relay
271        ai_model_costs: None,        // only supported in relay
272        ai_operation_type_map: None, // only supported in relay
273        enable_trimming: config.enable_trimming.unwrap_or_default(),
274        measurements: None,
275        normalize_spans: config.normalize_spans,
276        replay_id: config.replay_id,
277        span_allowed_hosts: &[],              // only supported in relay
278        span_op_defaults: Default::default(), // only supported in relay
279        performance_issues_spans: Default::default(),
280    };
281    normalize_event(&mut event, &normalization_config);
282
283    RelayStr::from_string(event.to_json()?)
284}
285
286/// Replaces invalid JSON generated by Python.
287#[unsafe(no_mangle)]
288#[relay_ffi::catch_unwind]
289pub unsafe extern "C" fn relay_translate_legacy_python_json(event: *mut RelayStr) -> bool {
290    let data = unsafe { slice::from_raw_parts_mut((*event).data as *mut u8, (*event).len) };
291    json_forensics::translate_slice(data);
292    true
293}
294
295/// Validates a PII selector spec. Used to validate datascrubbing safe fields.
296#[unsafe(no_mangle)]
297#[relay_ffi::catch_unwind]
298pub unsafe extern "C" fn relay_validate_pii_selector(value: *const RelayStr) -> RelayStr {
299    let value = unsafe { (*value).as_str() };
300    match value.parse::<SelectorSpec>() {
301        Ok(_) => RelayStr::new(""),
302        Err(err) => match err {
303            InvalidSelectorError::ParseError(_) => {
304                // Change the error to something more concise we can show in an UI.
305                // Error message follows the same format used for fingerprinting rules.
306                RelayStr::from_string(format!("invalid syntax near {value:?}"))
307            }
308            err => RelayStr::from_string(err.to_string()),
309        },
310    }
311}
312
313/// Validate a PII config against the schema. Used in project options UI.
314#[unsafe(no_mangle)]
315#[relay_ffi::catch_unwind]
316pub unsafe extern "C" fn relay_validate_pii_config(value: *const RelayStr) -> RelayStr {
317    match serde_json::from_str::<PiiConfig>(unsafe { (*value).as_str() }) {
318        Ok(config) => match config.compiled().force_compile() {
319            Ok(_) => RelayStr::new(""),
320            Err(PiiConfigError::RegexError(source)) => RelayStr::from_string(source.to_string()),
321        },
322        Err(e) => RelayStr::from_string(e.to_string()),
323    }
324}
325
326/// Convert an old datascrubbing config to the new PII config format.
327#[unsafe(no_mangle)]
328#[relay_ffi::catch_unwind]
329pub unsafe extern "C" fn relay_convert_datascrubbing_config(config: *const RelayStr) -> RelayStr {
330    let config: DataScrubbingConfig = serde_json::from_str(unsafe { (*config).as_str() })?;
331    match config.pii_config() {
332        Ok(Some(config)) => RelayStr::from_string(serde_json::to_string(config)?),
333        Ok(None) => RelayStr::new("{}"),
334        // NOTE: Callers of this function must be able to handle this error.
335        Err(e) => RelayStr::from_string(e.to_string()),
336    }
337}
338
339/// Scrub an event using new PII stripping config.
340#[unsafe(no_mangle)]
341#[relay_ffi::catch_unwind]
342pub unsafe extern "C" fn relay_pii_strip_event(
343    config: *const RelayStr,
344    event: *const RelayStr,
345) -> RelayStr {
346    let config = serde_json::from_str::<PiiConfig>(unsafe { (*config).as_str() })?;
347    let mut processor = PiiProcessor::new(config.compiled());
348
349    let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
350    process_value(&mut event, &mut processor, ProcessingState::root())?;
351
352    RelayStr::from_string(event.to_json()?)
353}
354
355/// Walk through the event and collect selectors that can be applied to it in a PII config. This
356/// function is used in the UI to provide auto-completion of selectors.
357#[unsafe(no_mangle)]
358#[relay_ffi::catch_unwind]
359pub unsafe extern "C" fn relay_pii_selector_suggestions_from_event(
360    event: *const RelayStr,
361) -> RelayStr {
362    let mut event = Annotated::<Event>::from_json(unsafe { (*event).as_str() })?;
363    let rv = selector_suggestions_from_value(&mut event);
364    RelayStr::from_string(serde_json::to_string(&rv)?)
365}
366
367/// A test function that always panics.
368#[unsafe(no_mangle)]
369#[relay_ffi::catch_unwind]
370#[allow(clippy::diverging_sub_expression)]
371pub unsafe extern "C" fn relay_test_panic() -> () {
372    panic!("this is a test panic")
373}
374
375/// Parse a sentry release structure from a string.
376#[unsafe(no_mangle)]
377#[relay_ffi::catch_unwind]
378pub unsafe extern "C" fn relay_parse_release(value: *const RelayStr) -> RelayStr {
379    let release = sentry_release_parser::Release::parse(unsafe { (*value).as_str() })?;
380    RelayStr::from_string(serde_json::to_string(&release)?)
381}
382
383/// Compares two versions.
384#[unsafe(no_mangle)]
385#[relay_ffi::catch_unwind]
386pub unsafe extern "C" fn relay_compare_versions(a: *const RelayStr, b: *const RelayStr) -> i32 {
387    let ver_a = sentry_release_parser::Version::parse(unsafe { (*a).as_str() })?;
388    let ver_b = sentry_release_parser::Version::parse(unsafe { (*b).as_str() })?;
389    match ver_a.cmp(&ver_b) {
390        Ordering::Less => -1,
391        Ordering::Equal => 0,
392        Ordering::Greater => 1,
393    }
394}
395
396/// Validate a dynamic rule condition.
397///
398/// Used by dynamic sampling, metric extraction, and metric tagging.
399#[unsafe(no_mangle)]
400#[relay_ffi::catch_unwind]
401pub unsafe extern "C" fn relay_validate_rule_condition(value: *const RelayStr) -> RelayStr {
402    let ret_val = match serde_json::from_str::<RuleCondition>(unsafe { (*value).as_str() }) {
403        Ok(condition) => {
404            if condition.supported() {
405                "".to_owned()
406            } else {
407                "unsupported condition".to_owned()
408            }
409        }
410        Err(e) => e.to_string(),
411    };
412    RelayStr::from_string(ret_val)
413}
414
415/// Validate whole rule ( this will be also implemented in Sentry for better error messages)
416/// The implementation in relay is just to make sure that the Sentry implementation doesn't
417/// go out of sync.
418#[unsafe(no_mangle)]
419#[relay_ffi::catch_unwind]
420pub unsafe extern "C" fn relay_validate_sampling_configuration(value: *const RelayStr) -> RelayStr {
421    match serde_json::from_str::<SamplingConfig>(unsafe { (*value).as_str() }) {
422        Ok(config) => {
423            for rule in config.rules {
424                if !rule.condition.supported() {
425                    return Ok(RelayStr::new("unsupported sampling rule"));
426                }
427            }
428            RelayStr::default()
429        }
430        Err(e) => RelayStr::from_string(e.to_string()),
431    }
432}
433
434/// Normalize a project config.
435#[unsafe(no_mangle)]
436#[relay_ffi::catch_unwind]
437pub unsafe extern "C" fn relay_normalize_project_config(value: *const RelayStr) -> RelayStr {
438    let value = unsafe { (*value).as_str() };
439    match normalize_json::<ProjectConfig>(value) {
440        Ok(normalized) => RelayStr::from_string(normalized),
441        Err(e) => RelayStr::from_string(e.to_string()),
442    }
443}
444
445/// Normalize a cardinality limit config.
446#[unsafe(no_mangle)]
447#[relay_ffi::catch_unwind]
448pub unsafe extern "C" fn normalize_cardinality_limit_config(value: *const RelayStr) -> RelayStr {
449    let value = unsafe { (*value).as_str() };
450    match normalize_json::<CardinalityLimit>(value) {
451        Ok(normalized) => RelayStr::from_string(normalized),
452        Err(e) => RelayStr::from_string(e.to_string()),
453    }
454}
455
456/// Normalize a global config.
457#[unsafe(no_mangle)]
458#[relay_ffi::catch_unwind]
459pub unsafe extern "C" fn relay_normalize_global_config(value: *const RelayStr) -> RelayStr {
460    let value = unsafe { (*value).as_str() };
461    match normalize_json::<GlobalConfig>(value) {
462        Ok(normalized) => RelayStr::from_string(normalized),
463        Err(e) => RelayStr::from_string(e.to_string()),
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn pii_config_validation_invalid_regex() {
473        let config = r#"
474        {
475          "rules": {
476            "strip-fields": {
477              "type": "redact_pair",
478              "keyPattern": "(not valid regex",
479              "redaction": {
480                "method": "replace",
481                "text": "[Filtered]"
482              }
483            }
484          },
485          "applications": {
486            "*.everything": ["strip-fields"]
487          }
488        }
489    "#;
490        unsafe {
491            assert_eq!(
492                relay_validate_pii_config(&RelayStr::from(config)).as_str(),
493                "regex parse error:\n    (not valid regex\n    ^\nerror: unclosed group"
494            );
495        }
496    }
497
498    #[test]
499    fn pii_config_validation_valid_regex() {
500        let config = r#"
501        {
502          "rules": {
503            "strip-fields": {
504              "type": "redact_pair",
505              "keyPattern": "(\\w+)?+",
506              "redaction": {
507                "method": "replace",
508                "text": "[Filtered]"
509              }
510            }
511          },
512          "applications": {
513            "*.everything": ["strip-fields"]
514          }
515        }
516    "#;
517        unsafe {
518            assert_eq!(
519                relay_validate_pii_config(&RelayStr::from(config)).as_str(),
520                ""
521            );
522        }
523    }
524}