1use crate::{
8 ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorReport, GlobalSettings, IndentationStyle,
9 LinkDebugIde, ResultExt, StringError, SubmitErrorReport,
10};
11use colored::Colorize;
12use std::sync::{OnceLock, RwLock, RwLockReadGuard};
13use url::{Url, form_urlencoded};
14
15#[derive(Debug, Clone)]
82pub struct GitLabErrorReport<'a> {
83 pub error_report: &'a ErrorReport,
85}
86
87impl<'a> SubmitErrorReport<'a> for GitLabErrorReport<'a> {
88 fn new(error: &'a ErrorReport) -> Self {
89 GitLabErrorReport {
90 error_report: error,
91 }
92 }
93
94 fn get_error_report(&self) -> &ErrorReport {
95 self.error_report
96 }
97
98 fn get_title(&self) -> String {
99 self.error_report.get_last_error_title()
100 }
101
102 fn create_message(&self) -> Result<String, ErrorReport> {
103 let step1 = "Step 1".yellow();
104 let step2 = "Step 2".yellow();
105 let bug_report_stdout = {
106 let settings = GitLabERGlobalSettings::get_global_settings()?;
107 let display_parts = ReportPartEnabled::display_report();
108 let format_settings = settings.release_format_settings_stderr;
109
110 self.create_new_bug_report(&settings, &display_parts, &format_settings)
111 }?;
112 Ok(format!(
113 "Panic Info:\n{}\n\
114 Please report this error. Issue title: `{}`\n\
115 \t- {step1}: Check if error report already exists: {}\n\
116 \t- {step2}: Report issue (if it does not already exists):\n\
117 \t- Open Pre-created Report Link: \n---\n{}\n----\n",
118 bug_report_stdout,
119 self.get_title().cyan().bold(),
120 self.check_existing_reports()?.to_string().green(),
121 self.create_submit_url()?.to_string().green(),
122 ))
123 }
124
125 fn create_bug_report(&self) -> Result<String, ErrorReport> {
126 let settings = GitLabERGlobalSettings::get_global_settings()?;
127 let display_parts = ReportPartEnabled::display_report();
128 let format_settings = ErrorFmtSettings {
129 level_of_detail: ErrorFmtLoD::SubmitReport,
130 frame_limit: None,
131 enable_color: false,
132 link_format: LinkDebugIde::NoLink,
133 indentation_style: IndentationStyle::Tab,
134 skip_first_indentations: 2,
135 ..Default::default()
136 };
137
138 self.create_new_bug_report(&settings, &display_parts, &format_settings)
139 }
140
141 fn create_submit_url_limited(&self, max_length: usize) -> Result<Url, ErrorReport> {
142 let settings = GitLabERGlobalSettings::get_global_settings()?;
143 let url_char_limit = std::cmp::min(max_length, settings.url_char_limit);
144
145 let minimal_link_parts = ReportPartEnabled::disable_all();
147 let minimal_url = self.create_new_submit_url(&settings, &minimal_link_parts)?;
148 if minimal_url.to_string().len() > url_char_limit {
149 tracing::warn!(
150 limit = url_char_limit,
151 size = minimal_url.to_string().len(),
152 "Minimal URL is already bigger then URL Limit",
153 );
154 return Ok(minimal_url);
155 }
156
157 let mut link_parts = ReportPartEnabled::default();
159
160 let mut url = self.create_new_submit_url(&settings, &link_parts)?;
162 if url.to_string().len() <= url_char_limit {
163 return Ok(url);
164 } else {
165 tracing::trace!(
167 url_length = url.to_string().len(),
168 limit = url_char_limit,
169 "URL to Long, Removing 'Steps' from Error Report URL."
170 );
171 link_parts.add_steps = false;
172 }
173
174 url = self.create_new_submit_url(&settings, &link_parts)?;
176 if url.to_string().len() <= url_char_limit {
177 return Ok(url);
178 } else {
179 tracing::trace!(
181 url_length = url.to_string().len(),
182 limit = url_char_limit,
183 "URL to Long, Removing 'Backtrace' from Error Report URL."
184 );
185 link_parts.add_backtrace = false;
186 }
187
188 url = self.create_new_submit_url(&settings, &link_parts)?;
190 if url.to_string().len() <= url_char_limit {
191 return Ok(url);
192 }
193
194 let mut error_frame_count = std::cmp::min(20, self.error_report.frames.len() - 1);
196 while error_frame_count > 0 && url.to_string().len() > url_char_limit {
197 link_parts.frame_limit = Some(error_frame_count);
198 url = self.create_new_submit_url(&settings, &link_parts)?;
199 tracing::trace!(
200 url_length = url.to_string().len(),
201 limit = url_char_limit,
202 error_frame_count = error_frame_count,
203 "URL to Long, Removing Some ErrorFrames from Error Report URL."
204 );
205 if url.to_string().len() <= url_char_limit {
207 return Ok(url);
208 }
209 error_frame_count -= 1;
210 }
211
212 link_parts.frame_limit = Some(1);
214 let url = self.create_new_submit_url(&settings, &link_parts)?;
215 if url.to_string().len() > url_char_limit {
216 tracing::error!(
217 url_length = url.to_string().len(),
218 limit = url_char_limit,
219 "ErrorReport even adding 1 ErrorFrame to URL will go over URL Limit."
220 );
221 }
222 Ok(url)
223 }
224
225 fn create_submit_url(&self) -> Result<Url, ErrorReport> {
226 self.create_submit_url_limited(usize::MAX)
227 }
228
229 fn check_existing_reports(&self) -> Result<Url, ErrorReport> {
230 let settings = GitLabERGlobalSettings::get_global_settings()?;
231 let search_title: String = form_urlencoded::Serializer::new(String::new())
232 .append_pair(
233 "search",
234 &Self::truncate_string(self.get_title(), settings.title_char_limit),
235 )
236 .finish();
237 Ok(Url::parse(&format!(
238 "https://{domain}/{project_path}/-/issues/?sort=created_date&state=all&in=TITLE&{search_title}",
239 domain = settings.domain,
240 project_path = settings.project_path,
241 search_title = search_title,
242 ))?)
243 }
244
245 fn validate_settings(&self) -> Result<(), ErrorReport> {
246 if !GitLabERGlobalSettings::is_set() {
247 Err(StringError::new(
248 "GitLabERGlobalSettings has not been set, this could create runtime issues.",
249 )
250 .into())
251 } else {
252 Ok(())
253 }
254 }
255}
256
257#[derive(Debug, Clone)]
262pub struct ReportPartEnabled {
263 pub add_steps: bool,
265 pub add_backtrace: bool,
267 pub add_summary: bool,
269 pub frame_limit: Option<usize>,
271 pub enable_gitlab_syntax: bool,
273}
274
275impl ReportPartEnabled {
276 fn disable_all() -> Self {
277 Self {
278 add_steps: false,
279 add_backtrace: false,
280 add_summary: false,
281 frame_limit: Some(0),
282 enable_gitlab_syntax: true,
283 }
284 }
285
286 fn display_report() -> Self {
287 Self {
288 add_steps: false,
289 add_backtrace: true,
290 add_summary: true,
291 frame_limit: None,
292 enable_gitlab_syntax: false,
293 }
294 }
295}
296
297impl Default for ReportPartEnabled {
298 fn default() -> Self {
299 Self {
300 add_steps: true,
301 add_backtrace: true,
302 add_summary: true,
303 frame_limit: None,
304 enable_gitlab_syntax: true,
305 }
306 }
307}
308
309impl<'a> GitLabErrorReport<'a> {
310 fn gitlab_syntax<S: Into<String> + Default>(text: S, enabled_parts: &ReportPartEnabled) -> S {
311 if enabled_parts.enable_gitlab_syntax {
312 text
313 } else {
314 S::default()
315 }
316 }
317
318 fn create_new_bug_report(
319 &self,
320 settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>,
321 enabled_parts: &ReportPartEnabled,
322 format_settings: &ErrorFmtSettings,
323 ) -> Result<String, ErrorReport> {
324 let reproduce = if enabled_parts.add_steps {
325 "## Reproduce:\n\
326 Steps to recreate this issue:\n\
327 1. ...\n\
328 2. ...\n\n"
329 } else {
330 ""
331 };
332 let backtrace = if enabled_parts.add_backtrace {
333 let backtrace_string = self
334 .error_report
335 .frames
336 .last()
337 .unwrap_error()
338 .create_backtrace_string();
339 format!(
340 "### Backtrace:\n\
341 {start_summary}\
342 ```\n\
343 {backtrace}\n\
344 ```\n\n\
345 {end_summary}",
346 start_summary = Self::gitlab_syntax(
347 "<details><summary markdown=\"span\">Backtrace</summary>\n\n",
348 enabled_parts
349 ),
350 end_summary = Self::gitlab_syntax("</details>\n\n", enabled_parts),
351 backtrace = backtrace_string,
352 )
353 } else {
354 "".to_owned()
355 };
356
357 let summary = if enabled_parts.add_summary {
358 let mut format_settings = *format_settings;
359 format_settings.frame_limit = enabled_parts.frame_limit;
360 format!(
361 "```hcl\n\
362 {message}\n\
363 ```\n\n",
364 message = self.error_report.stringify(format_settings)?
365 )
366 } else {
367 "".to_owned()
368 };
369
370 Ok(format!(
371 "{header}\
372 ## Summary:\n\
373 {summary}\
374 {reproduce}\
375 ## Debug info:\n\
376 {backtrace}\
377 ### System:\n\
378 * Application version: {version}\n\
379 * System: {os} {arch}\n\
380 {start_footer}\
381 {labels}",
382 header = Self::gitlab_syntax(
383 "<!-- Please check above for similar title.\n\
384 Someone might have already reported this.-->\n",
385 enabled_parts
386 ),
387 summary = summary,
388 reproduce = reproduce,
389 backtrace = backtrace,
390 version = env!("CARGO_PKG_VERSION"),
391 arch = std::env::consts::ARCH,
392 os = std::env::consts::OS,
393 start_footer = Self::gitlab_syntax("<!-- Leave below untouched! -->\n", enabled_parts),
394 labels = Self::gitlab_syntax(self.get_labels(settings), enabled_parts),
395 ))
396 }
397
398 fn create_new_submit_url(
399 &self,
400 settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>,
401 enabled_parts: &ReportPartEnabled,
402 ) -> Result<Url, ErrorReport> {
403 let format_settings = ErrorFmtSettings {
404 level_of_detail: ErrorFmtLoD::SubmitReport,
405 frame_limit: None,
406 enable_color: false,
407 link_format: LinkDebugIde::NoLink,
408 indentation_style: IndentationStyle::Tab,
409 skip_first_indentations: 2,
410 ..Default::default()
411 };
412 let description = self.create_new_bug_report(settings, enabled_parts, &format_settings)?;
413
414 let encoded: String = form_urlencoded::Serializer::new(String::new())
415 .append_pair(
416 "issue[title]",
417 &Self::truncate_string(self.get_title(), settings.title_char_limit),
418 )
419 .append_pair(
420 "issue[description]",
421 &Self::truncate_string(description, settings.description_char_limit),
422 )
423 .finish();
424
425 Ok(Url::parse(&format!(
427 "https://{domain}/{project_path}/issues/new?{encoded_message}",
428 domain = settings.domain,
429 project_path = settings.project_path,
430 encoded_message = encoded,
431 ))?)
432 }
433
434 fn get_labels(&self, settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>) -> String {
435 let mut labels_string = "/label".to_owned();
436 for label in &settings.labels {
437 labels_string = format!("{labels_string} ~\"{label}\"");
438 }
439 labels_string
440 }
441}
442
443#[derive(Debug, Clone)]
448pub struct GitLabERGlobalSettings {
449 pub domain: String,
451 pub project_path: String,
453 pub labels: Vec<String>,
455 pub url_char_limit: usize,
458 pub title_char_limit: usize,
462 pub description_char_limit: usize,
466 pub release_format_settings_stderr: ErrorFmtSettings,
474}
475
476impl GlobalSettings for GitLabERGlobalSettings {
477 type Setting = GitLabERGlobalSettings;
478
479 fn once_lock() -> &'static OnceLock<RwLock<Self::Setting>> {
480 &GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG
481 }
482 fn get_setting_object_name() -> &'static str {
483 "GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG"
484 }
485}
486
487static GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG: OnceLock<RwLock<GitLabERGlobalSettings>> =
488 OnceLock::new();
489
490impl Default for GitLabERGlobalSettings {
491 fn default() -> Self {
492 Self {
493 domain: "gitlab.com".to_owned(),
494 project_path: "<not set>".to_owned(),
495 labels: vec!["Auto Generated Issue".to_owned()],
496 url_char_limit: 2085,
497 title_char_limit: 1024,
498 description_char_limit: 1_048_576, release_format_settings_stderr: ErrorFmtSettings {
500 level_of_detail: ErrorFmtLoD::SubmitReport,
501 frame_limit: None,
502 enable_color: false,
503 link_format: LinkDebugIde::NoLink,
504 indentation_style: IndentationStyle::Tab,
505 skip_first_indentations: 2,
506 ..Default::default()
507 },
508 }
509 }
510}