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