Skip to main content

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