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