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