1use std::error::Error;
43use std::net::IpAddr;
44use std::time::Duration;
45
46use bytes::Bytes;
47
48use relay_dynamic_config::GlobalConfig;
49use relay_event_schema::protocol::{Event, EventId};
50use relay_filter::{Filterable, ProjectFiltersConfig};
51use relay_protocol::{Getter, Val};
52use serde::Deserialize;
53use serde_json::Deserializer;
54
55use crate::extract_from_transaction::{extract_transaction_metadata, extract_transaction_tags};
56
57pub use crate::error::ProfileError;
58pub use crate::outcomes::discard_reason;
59
60mod android;
61mod debug_image;
62mod error;
63mod extract_from_transaction;
64mod measurements;
65mod outcomes;
66mod sample;
67mod transaction_metadata;
68mod types;
69mod utils;
70
71const MAX_PROFILE_DURATION: Duration = Duration::from_secs(30);
72const MAX_PROFILE_CHUNK_DURATION: Duration = Duration::from_secs(66);
77
78pub type ProfileId = EventId;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum ProfileType {
86 Backend,
88 Ui,
90}
91
92impl ProfileType {
93 pub fn from_platform(platform: &str) -> Self {
101 match platform {
102 "cocoa" | "android" | "javascript" => Self::Ui,
103 _ => Self::Backend,
104 }
105 }
106}
107
108#[derive(Debug, Deserialize)]
109struct MinimalProfile {
110 #[serde(alias = "profile_id", alias = "chunk_id")]
111 event_id: ProfileId,
112 platform: String,
113 release: Option<String>,
114 #[serde(default)]
115 version: sample::Version,
116}
117
118impl Filterable for MinimalProfile {
119 fn release(&self) -> Option<&str> {
120 self.release.as_deref()
121 }
122}
123
124impl Getter for MinimalProfile {
125 fn get_value(&self, path: &str) -> Option<Val<'_>> {
126 match path.strip_prefix("event.")? {
127 "release" => self.release.as_deref().map(|release| release.into()),
128 "platform" => Some(self.platform.as_str().into()),
129 _ => None,
130 }
131 }
132}
133
134fn minimal_profile_from_json(
135 payload: &[u8],
136) -> Result<MinimalProfile, serde_path_to_error::Error<serde_json::Error>> {
137 let d = &mut Deserializer::from_slice(payload);
138 serde_path_to_error::deserialize(d)
139}
140
141#[derive(Debug)]
143pub struct ProfileMetadata {
144 pub id: ProfileId,
145 pub platform: String,
146}
147
148impl ProfileMetadata {
149 pub fn profile_type(&self) -> ProfileType {
153 ProfileType::from_platform(&self.platform)
154 }
155}
156
157pub fn parse_metadata(payload: &[u8]) -> Result<ProfileMetadata, ProfileError> {
158 let profile = match minimal_profile_from_json(payload) {
159 Ok(profile) => profile,
160 Err(err) => {
161 relay_log::debug!(
162 error = &err as &dyn Error,
163 from = "minimal",
164 "invalid profile"
165 );
166 return Err(ProfileError::InvalidJson(err));
167 }
168 };
169 match profile.version {
170 sample::Version::V1 => {
171 let d = &mut Deserializer::from_slice(payload);
172 let _: sample::v1::ProfileMetadata = match serde_path_to_error::deserialize(d) {
173 Ok(profile) => profile,
174 Err(err) => {
175 relay_log::debug!(
176 error = &err as &dyn Error,
177 from = "metadata",
178 platform = profile.platform,
179 "invalid profile",
180 );
181 return Err(ProfileError::InvalidJson(err));
182 }
183 };
184 }
185 _ => match profile.platform.as_str() {
186 "android" => {
187 let d = &mut Deserializer::from_slice(payload);
188 let _: android::legacy::ProfileMetadata = match serde_path_to_error::deserialize(d)
189 {
190 Ok(profile) => profile,
191 Err(err) => {
192 relay_log::debug!(
193 error = &err as &dyn Error,
194 from = "metadata",
195 platform = "android",
196 "invalid profile",
197 );
198 return Err(ProfileError::InvalidJson(err));
199 }
200 };
201 }
202 _ => return Err(ProfileError::PlatformNotSupported),
203 },
204 };
205
206 Ok(ProfileMetadata {
207 id: profile.event_id,
208 platform: profile.platform,
209 })
210}
211
212pub fn expand_profile(
213 payload: &[u8],
214 event: &Event,
215 client_ip: Option<IpAddr>,
216 filter_settings: &ProjectFiltersConfig,
217 global_config: &GlobalConfig,
218) -> Result<(ProfileId, Vec<u8>), ProfileError> {
219 let profile = match minimal_profile_from_json(payload) {
220 Ok(profile) => profile,
221 Err(err) => {
222 relay_log::debug!(
223 error = &err as &dyn Error,
224 from = "minimal",
225 platform = event.platform.as_str(),
226 project_id = event.project.value().unwrap_or(&0),
227 sdk_name = event.sdk_name(),
228 sdk_version = event.sdk_version(),
229 transaction_id = ?event.id.value(),
230 "invalid profile",
231 );
232 return Err(ProfileError::InvalidJson(err));
233 }
234 };
235
236 if let Err(filter_stat_key) = relay_filter::should_filter(
237 &profile,
238 client_ip,
239 filter_settings,
240 global_config.filters(),
241 ) {
242 return Err(ProfileError::Filtered(filter_stat_key));
243 }
244
245 let transaction_metadata = extract_transaction_metadata(event);
246 let transaction_tags = extract_transaction_tags(event);
247 let processed_payload = match (profile.platform.as_str(), profile.version) {
248 (_, sample::Version::V1) => {
249 sample::v1::parse_sample_profile(payload, transaction_metadata, transaction_tags)
250 }
251 ("android", _) => {
252 android::legacy::parse_android_profile(payload, transaction_metadata, transaction_tags)
253 }
254 (_, _) => return Err(ProfileError::PlatformNotSupported),
255 };
256 match processed_payload {
257 Ok(payload) => Ok((profile.event_id, payload)),
258 Err(err) => match err {
259 ProfileError::InvalidJson(err) => {
260 relay_log::debug!(
261 error = &err as &dyn Error,
262 from = "parsing",
263 platform = profile.platform,
264 project_id = event.project.value().unwrap_or(&0),
265 sdk_name = event.sdk_name(),
266 sdk_version = event.sdk_version(),
267 transaction_id = ?event.id.value(),
268 "invalid profile",
269 );
270 Err(ProfileError::InvalidJson(err))
271 }
272 _ => {
273 relay_log::debug!(
274 error = &err as &dyn Error,
275 from = "parsing",
276 platform = profile.platform,
277 project_id = event.project.value().unwrap_or(&0),
278 sdk_name = event.sdk_name(),
279 sdk_version = event.sdk_version(),
280 transaction_id = ?event.id.value(),
281 "invalid profile",
282 );
283 Err(err)
284 }
285 },
286 }
287}
288
289pub struct ProfileChunk {
291 profile: MinimalProfile,
292 payload: Bytes,
293}
294
295impl ProfileChunk {
296 pub fn new(payload: Bytes) -> Result<Self, ProfileError> {
298 match minimal_profile_from_json(&payload) {
299 Ok(profile) => Ok(Self { profile, payload }),
300 Err(err) => {
301 relay_log::debug!(
302 error = &err as &dyn Error,
303 from = "minimal",
304 "invalid profile chunk",
305 );
306 Err(ProfileError::InvalidJson(err))
307 }
308 }
309 }
310
311 pub fn platform(&self) -> &str {
313 &self.profile.platform
314 }
315
316 pub fn profile_type(&self) -> ProfileType {
320 ProfileType::from_platform(&self.profile.platform)
321 }
322
323 pub fn filter(
327 &self,
328 client_ip: Option<IpAddr>,
329 filter_settings: &ProjectFiltersConfig,
330 global_config: &GlobalConfig,
331 ) -> Result<(), ProfileError> {
332 relay_filter::should_filter(
333 &self.profile,
334 client_ip,
335 filter_settings,
336 global_config.filters(),
337 )
338 .map_err(ProfileError::Filtered)
339 }
340
341 pub fn expand(&self) -> Result<Vec<u8>, ProfileError> {
343 match (self.profile.platform.as_str(), self.profile.version) {
344 ("android", _) => android::chunk::parse(&self.payload),
345 (_, sample::Version::V2) => {
346 let mut profile = sample::v2::parse(&self.payload)?;
347 profile.normalize()?;
348 Ok(serde_json::to_vec(&profile)
349 .map_err(|_| ProfileError::CannotSerializePayload)?)
350 }
351 (_, _) => Err(ProfileError::PlatformNotSupported),
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_minimal_profile_with_version() {
362 let data = r#"{"version":"1","platform":"cocoa","event_id":"751fff80-a266-467b-a6f5-eeeef65f4f84"}"#;
363 let profile = minimal_profile_from_json(data.as_bytes());
364 assert!(profile.is_ok());
365 assert_eq!(profile.unwrap().version, sample::Version::V1);
366 }
367
368 #[test]
369 fn test_minimal_profile_without_version() {
370 let data = r#"{"platform":"android","event_id":"751fff80-a266-467b-a6f5-eeeef65f4f84"}"#;
371 let profile = minimal_profile_from_json(data.as_bytes());
372 assert!(profile.is_ok());
373 assert_eq!(profile.unwrap().version, sample::Version::Unknown);
374 }
375
376 #[test]
377 fn test_expand_profile_with_version() {
378 let payload = include_bytes!("../tests/fixtures/sample/v1/valid.json");
379 assert!(
380 expand_profile(
381 payload,
382 &Event::default(),
383 None,
384 &ProjectFiltersConfig::default(),
385 &GlobalConfig::default()
386 )
387 .is_ok()
388 );
389 }
390
391 #[test]
392 fn test_expand_profile_with_version_and_segment_id() {
393 let payload = include_bytes!("../tests/fixtures/sample/v1/segment_id.json");
394 assert!(
395 expand_profile(
396 payload,
397 &Event::default(),
398 None,
399 &ProjectFiltersConfig::default(),
400 &GlobalConfig::default()
401 )
402 .is_ok()
403 );
404 }
405
406 #[test]
407 fn test_expand_profile_without_version() {
408 let payload = include_bytes!("../tests/fixtures/android/legacy/roundtrip.json");
409 assert!(
410 expand_profile(
411 payload,
412 &Event::default(),
413 None,
414 &ProjectFiltersConfig::default(),
415 &GlobalConfig::default()
416 )
417 .is_ok()
418 );
419 }
420}