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 kind: ProfileType,
146}
147
148pub fn parse_metadata(payload: &[u8]) -> Result<ProfileMetadata, ProfileError> {
149 let profile = match minimal_profile_from_json(payload) {
150 Ok(profile) => profile,
151 Err(err) => {
152 relay_log::debug!(error = &err as &dyn Error, from = "minimal",);
153 return Err(ProfileError::InvalidJson(err));
154 }
155 };
156 match profile.version {
157 sample::Version::V1 => {
158 let d = &mut Deserializer::from_slice(payload);
159 let _: sample::v1::ProfileMetadata = match serde_path_to_error::deserialize(d) {
160 Ok(profile) => profile,
161 Err(err) => {
162 relay_log::debug!(
163 error = &err as &dyn Error,
164 from = "metadata",
165 platform = profile.platform,
166 "invalid profile",
167 );
168 return Err(ProfileError::InvalidJson(err));
169 }
170 };
171 }
172 _ => match profile.platform.as_str() {
173 "android" => {
174 let d = &mut Deserializer::from_slice(payload);
175 let _: android::legacy::ProfileMetadata = match serde_path_to_error::deserialize(d)
176 {
177 Ok(profile) => profile,
178 Err(err) => {
179 relay_log::debug!(
180 error = &err as &dyn Error,
181 from = "metadata",
182 platform = "android",
183 "invalid profile",
184 );
185 return Err(ProfileError::InvalidJson(err));
186 }
187 };
188 }
189 _ => return Err(ProfileError::PlatformNotSupported),
190 },
191 };
192
193 Ok(ProfileMetadata {
194 id: profile.event_id,
195 kind: ProfileType::from_platform(&profile.platform),
196 })
197}
198
199pub fn expand_profile(
200 payload: &[u8],
201 event: &Event,
202 client_ip: Option<IpAddr>,
203 filter_settings: &ProjectFiltersConfig,
204 global_config: &GlobalConfig,
205) -> Result<(ProfileId, Vec<u8>), ProfileError> {
206 let profile = match minimal_profile_from_json(payload) {
207 Ok(profile) => profile,
208 Err(err) => {
209 relay_log::debug!(
210 error = &err as &dyn Error,
211 from = "minimal",
212 platform = event.platform.as_str(),
213 project_id = event.project.value().unwrap_or(&0),
214 sdk_name = event.sdk_name(),
215 sdk_version = event.sdk_version(),
216 transaction_id = ?event.id.value(),
217 "invalid profile",
218 );
219 return Err(ProfileError::InvalidJson(err));
220 }
221 };
222
223 if let Err(filter_stat_key) = relay_filter::should_filter(
224 &profile,
225 client_ip,
226 filter_settings,
227 global_config.filters(),
228 ) {
229 return Err(ProfileError::Filtered(filter_stat_key));
230 }
231
232 let transaction_metadata = extract_transaction_metadata(event);
233 let transaction_tags = extract_transaction_tags(event);
234 let processed_payload = match (profile.platform.as_str(), profile.version) {
235 (_, sample::Version::V1) => {
236 sample::v1::parse_sample_profile(payload, transaction_metadata, transaction_tags)
237 }
238 ("android", _) => {
239 android::legacy::parse_android_profile(payload, transaction_metadata, transaction_tags)
240 }
241 (_, _) => return Err(ProfileError::PlatformNotSupported),
242 };
243 match processed_payload {
244 Ok(payload) => Ok((profile.event_id, payload)),
245 Err(err) => match err {
246 ProfileError::InvalidJson(err) => {
247 relay_log::debug!(
248 error = &err as &dyn Error,
249 from = "parsing",
250 platform = profile.platform,
251 project_id = event.project.value().unwrap_or(&0),
252 sdk_name = event.sdk_name(),
253 sdk_version = event.sdk_version(),
254 transaction_id = ?event.id.value(),
255 "invalid profile",
256 );
257 Err(ProfileError::InvalidJson(err))
258 }
259 _ => {
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(err)
271 }
272 },
273 }
274}
275
276pub struct ProfileChunk {
278 profile: MinimalProfile,
279 payload: Bytes,
280}
281
282impl ProfileChunk {
283 pub fn new(payload: Bytes) -> Result<Self, ProfileError> {
285 match minimal_profile_from_json(&payload) {
286 Ok(profile) => Ok(Self { profile, payload }),
287 Err(err) => {
288 relay_log::debug!(
289 error = &err as &dyn Error,
290 from = "minimal",
291 "invalid profile chunk",
292 );
293 Err(ProfileError::InvalidJson(err))
294 }
295 }
296 }
297
298 pub fn profile_type(&self) -> ProfileType {
302 ProfileType::from_platform(&self.profile.platform)
303 }
304
305 pub fn filter(
309 &self,
310 client_ip: Option<IpAddr>,
311 filter_settings: &ProjectFiltersConfig,
312 global_config: &GlobalConfig,
313 ) -> Result<(), ProfileError> {
314 relay_filter::should_filter(
315 &self.profile,
316 client_ip,
317 filter_settings,
318 global_config.filters(),
319 )
320 .map_err(ProfileError::Filtered)
321 }
322
323 pub fn expand(&self) -> Result<Vec<u8>, ProfileError> {
325 match (self.profile.platform.as_str(), self.profile.version) {
326 ("android", _) => android::chunk::parse(&self.payload),
327 (_, sample::Version::V2) => {
328 let mut profile = sample::v2::parse(&self.payload)?;
329 profile.normalize()?;
330 Ok(serde_json::to_vec(&profile)
331 .map_err(|_| ProfileError::CannotSerializePayload)?)
332 }
333 (_, _) => Err(ProfileError::PlatformNotSupported),
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_minimal_profile_with_version() {
344 let data = r#"{"version":"1","platform":"cocoa","event_id":"751fff80-a266-467b-a6f5-eeeef65f4f84"}"#;
345 let profile = minimal_profile_from_json(data.as_bytes());
346 assert!(profile.is_ok());
347 assert_eq!(profile.unwrap().version, sample::Version::V1);
348 }
349
350 #[test]
351 fn test_minimal_profile_without_version() {
352 let data = r#"{"platform":"android","event_id":"751fff80-a266-467b-a6f5-eeeef65f4f84"}"#;
353 let profile = minimal_profile_from_json(data.as_bytes());
354 assert!(profile.is_ok());
355 assert_eq!(profile.unwrap().version, sample::Version::Unknown);
356 }
357
358 #[test]
359 fn test_expand_profile_with_version() {
360 let payload = include_bytes!("../tests/fixtures/sample/v1/valid.json");
361 assert!(
362 expand_profile(
363 payload,
364 &Event::default(),
365 None,
366 &ProjectFiltersConfig::default(),
367 &GlobalConfig::default()
368 )
369 .is_ok()
370 );
371 }
372
373 #[test]
374 fn test_expand_profile_with_version_and_segment_id() {
375 let payload = include_bytes!("../tests/fixtures/sample/v1/segment_id.json");
376 assert!(
377 expand_profile(
378 payload,
379 &Event::default(),
380 None,
381 &ProjectFiltersConfig::default(),
382 &GlobalConfig::default()
383 )
384 .is_ok()
385 );
386 }
387
388 #[test]
389 fn test_expand_profile_without_version() {
390 let payload = include_bytes!("../tests/fixtures/android/legacy/roundtrip.json");
391 assert!(
392 expand_profile(
393 payload,
394 &Event::default(),
395 None,
396 &ProjectFiltersConfig::default(),
397 &GlobalConfig::default()
398 )
399 .is_ok()
400 );
401 }
402}