1use std::sync::{OnceLock, RwLock, RwLockReadGuard};
2use url::{Url, form_urlencoded};
3
4use crate::{
5 ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorReport, IndentationStyle, LinkDebugIde,
6 ResultExt, StringError, SubmitErrorReport,
7};
8
9#[derive(Debug, Clone)]
28pub struct GitLabErrorReport<'a> {
29 pub error_report: &'a ErrorReport,
31}
32
33impl<'a> SubmitErrorReport<'a> for GitLabErrorReport<'a> {
34 fn new(error: &'a ErrorReport) -> Self {
35 GitLabErrorReport {
36 error_report: error,
37 }
38 }
39
40 fn get_error_report(&self) -> &ErrorReport {
41 self.error_report
42 }
43
44 fn get_title(&self) -> String {
45 self.error_report.get_last_error_title()
46 }
47
48 fn create_message(&self) -> Result<String, ErrorReport> {
49 Ok(format!(
50 "Panic Info:\n{}\n\
51 Please report this error. Issue title: `{}`\n\
52 \t- Step 1: Check if error report already exists: {}\n\
53 \t- Step 2: Report issue (if it does not already exists):\n\
54 \t- Open Pre-created Report Link: {}",
55 self.create_bug_report()?,
56 self.get_title(),
57 self.check_existing_reports()?,
58 self.create_submit_url()?,
59 ))
60 }
61
62 fn create_bug_report(&self) -> Result<String, ErrorReport> {
63 let settings = Self::get_global_config()?;
64 let display_parts = ReportPartEnabled::display_report();
65
66 self.create_new_bug_report(&settings, &display_parts)
67 }
68
69 fn create_submit_url_limited(&self, max_length: usize) -> Result<Url, ErrorReport> {
70 let settings = Self::get_global_config()?;
71 let url_char_limit = std::cmp::min(max_length, settings.url_char_limit);
72
73 let minimal_link_parts = ReportPartEnabled::disable_all();
75 let minimal_url = self.create_new_submit_url(&settings, &minimal_link_parts)?;
76 if minimal_url.to_string().len() > url_char_limit {
77 tracing::warn!(
78 limit = url_char_limit,
79 size = minimal_url.to_string().len(),
80 "Minimal URL is already bigger then URL Limit",
81 );
82 return Ok(minimal_url);
83 }
84
85 let mut link_parts = ReportPartEnabled::default();
87
88 let mut url = self.create_new_submit_url(&settings, &link_parts)?;
90 if url.to_string().len() <= url_char_limit {
91 return Ok(url);
92 } else {
93 tracing::trace!(
95 url_length = url.to_string().len(),
96 limit = url_char_limit,
97 "URL to Long, Removing 'Steps' from Error Report URL."
98 );
99 link_parts.add_steps = false;
100 }
101
102 url = self.create_new_submit_url(&settings, &link_parts)?;
104 if url.to_string().len() <= url_char_limit {
105 return Ok(url);
106 } else {
107 tracing::trace!(
109 url_length = url.to_string().len(),
110 limit = url_char_limit,
111 "URL to Long, Removing 'Backtrace' from Error Report URL."
112 );
113 link_parts.add_backtrace = false;
114 }
115
116 url = self.create_new_submit_url(&settings, &link_parts)?;
118 if url.to_string().len() <= url_char_limit {
119 return Ok(url);
120 }
121
122 let mut error_frame_count = std::cmp::min(20, self.error_report.frames.len() - 1);
124 while error_frame_count > 0 && url.to_string().len() > url_char_limit {
125 link_parts.frame_limit = Some(error_frame_count);
126 url = self.create_new_submit_url(&settings, &link_parts)?;
127 tracing::trace!(
128 url_length = url.to_string().len(),
129 limit = url_char_limit,
130 error_frame_count = error_frame_count,
131 "URL to Long, Removing Some ErrorFrames from Error Report URL."
132 );
133 if url.to_string().len() <= url_char_limit {
135 return Ok(url);
136 }
137 error_frame_count -= 1;
138 }
139
140 link_parts.frame_limit = Some(1);
142 let url = self.create_new_submit_url(&settings, &link_parts)?;
143 if url.to_string().len() > url_char_limit {
144 tracing::error!(
145 url_length = url.to_string().len(),
146 limit = url_char_limit,
147 "ErrorReport even adding 1 ErrorFrame to URL will go over URL Limit."
148 );
149 }
150 Ok(url)
151 }
152
153 fn create_submit_url(&self) -> Result<Url, ErrorReport> {
154 self.create_submit_url_limited(usize::MAX)
155 }
156
157 fn check_existing_reports(&self) -> Result<Url, ErrorReport> {
158 let settings = Self::get_global_config()?;
159 Ok(Url::parse(&format!(
160 "https://{domain}/{project_path}/-/issues/?sort=created_date&state=all&in=TITLE&search={title}",
161 domain = settings.domain,
162 project_path = settings.project_path,
163 title = Self::truncate_string(self.get_title(), settings.title_char_limit)
164 ))?)
165 }
166}
167
168#[derive(Debug, Clone)]
169pub struct ReportPartEnabled {
170 pub add_steps: bool,
171 pub add_backtrace: bool,
172 pub add_summary: bool,
173 pub frame_limit: Option<usize>,
174 pub enable_gitlab_syntax: bool,
175}
176
177impl ReportPartEnabled {
178 fn disable_all() -> Self {
179 Self {
180 add_steps: false,
181 add_backtrace: false,
182 add_summary: false,
183 frame_limit: Some(0),
184 enable_gitlab_syntax: true,
185 }
186 }
187
188 fn display_report() -> Self {
189 Self {
190 add_steps: false,
191 add_backtrace: true,
192 add_summary: true,
193 frame_limit: None,
194 enable_gitlab_syntax: false,
195 }
196 }
197}
198
199impl Default for ReportPartEnabled {
200 fn default() -> Self {
201 Self {
202 add_steps: true,
203 add_backtrace: true,
204 add_summary: true,
205 frame_limit: None,
206 enable_gitlab_syntax: true,
207 }
208 }
209}
210
211impl<'a> GitLabErrorReport<'a> {
212 fn gitlab_syntax<S: Into<String> + Default>(text: S, enabled_parts: &ReportPartEnabled) -> S {
213 if enabled_parts.enable_gitlab_syntax {
214 text
215 } else {
216 S::default()
217 }
218 }
219
220 fn create_new_bug_report(
221 &self,
222 settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>,
223 enabled_parts: &ReportPartEnabled,
224 ) -> Result<String, ErrorReport> {
225 let reproduce = if enabled_parts.add_steps {
226 "## Reproduce:\n\
227 Steps to recreate this issue:\n\
228 1. ...\n\
229 2. ...\n\n"
230 } else {
231 ""
232 };
233 let backtrace = if enabled_parts.add_backtrace {
234 let backtrace_string = self
235 .error_report
236 .frames
237 .last()
238 .unwrap_error()
239 .create_backtrace_string();
240 format!(
241 "### Backtrace:\n\
242 {start_summary}\
243 ```\n\
244 {backtrace}\n\
245 ```\n\n\
246 {end_summary}",
247 start_summary = Self::gitlab_syntax(
248 "<details><summary markdown=\"span\">Backtrace</summary>\n\n",
249 enabled_parts
250 ),
251 end_summary = Self::gitlab_syntax("</details>\n\n", enabled_parts),
252 backtrace = backtrace_string,
253 )
254 } else {
255 "".to_owned()
256 };
257
258 let summary = if enabled_parts.add_summary {
259 format!(
260 "```hcl\n\
261 {message}\n\
262 ```\n\n",
263 message = self.error_report.stringify(ErrorFmtSettings {
264 level_of_detail: ErrorFmtLoD::SubmitReport,
265 frame_limit: enabled_parts.frame_limit,
266 enable_color: false,
267 link_format: LinkDebugIde::NoLink,
268 indentation_style: IndentationStyle::Tab,
269 skip_first_indentations: 2,
270 ..Default::default()
271 })?
272 )
273 } else {
274 "".to_owned()
275 };
276
277 Ok(format!(
278 "{header}\
279 ## Summary:\n\
280 {summary}\
281 {reproduce}\
282 ## Debug info:\n\
283 {backtrace}\
284 ### System:\n\
285 * Application version: {version}\n\
286 * System: {os} {arch}\n\
287 {start_footer}\
288 {labels}",
289 header = Self::gitlab_syntax(
290 "<!-- Please check above for similar title.\n\
291 Someone might have already reported this.-->\n",
292 enabled_parts
293 ),
294 summary = summary,
295 reproduce = reproduce,
296 backtrace = backtrace,
297 version = env!("CARGO_PKG_VERSION"),
298 arch = std::env::consts::ARCH,
299 os = std::env::consts::OS,
300 start_footer = Self::gitlab_syntax("<!-- Leave below untouched! -->\n", enabled_parts),
301 labels = Self::gitlab_syntax(self.get_labels(settings), enabled_parts),
302 ))
303 }
304
305 fn create_new_submit_url(
306 &self,
307 settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>,
308 enabled_parts: &ReportPartEnabled,
309 ) -> Result<Url, ErrorReport> {
310 let description = self.create_new_bug_report(settings, enabled_parts)?;
311
312 let encoded: String = form_urlencoded::Serializer::new(String::new())
313 .append_pair(
314 "issue[title]",
315 &Self::truncate_string(self.get_title(), settings.title_char_limit),
316 )
317 .append_pair(
318 "issue[description]",
319 &Self::truncate_string(description, settings.description_char_limit),
320 )
321 .finish();
322
323 Ok(Url::parse(&format!(
325 "https://{domain}/{project_path}/issues/new?{encoded_message}",
326 domain = settings.domain,
327 project_path = settings.project_path,
328 encoded_message = encoded,
329 ))?)
330 }
331
332 fn get_labels(&self, settings: &RwLockReadGuard<'static, GitLabERGlobalSettings>) -> String {
333 let mut labels_string = "/label".to_owned();
334 for label in &settings.labels {
335 labels_string = format!("{labels_string} ~\"{label}\"");
336 }
337 labels_string
338 }
339
340 pub fn setup_global_config(config: GitLabERGlobalSettings) -> Result<(), ErrorReport> {
344 GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG
345 .set(RwLock::new(config))
346 .map_err(|_| StringError::new("`GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG` was already set"))
347 .change_context(GlobalSettingsError::AlreadySet)
348 }
349
350 pub fn get_global_config()
352 -> Result<RwLockReadGuard<'static, GitLabERGlobalSettings>, ErrorReport> {
353 let setting_reader = GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG
354 .get()
355 .ok_or(GlobalSettingsError::SettingNotYetSet)?;
356 setting_reader
358 .read()
359 .map_err(StringError::from_error)
360 .change_context(GlobalSettingsError::AcquireReadLockFailed)
361 }
362}
363
364static GITLAB_SUBMIT_REPORT_GLOBAL_CONFIG: OnceLock<RwLock<GitLabERGlobalSettings>> =
365 OnceLock::new();
366
367#[derive(Debug, Clone)]
372pub struct GitLabERGlobalSettings {
373 pub domain: String,
375 pub project_path: String,
377 pub labels: Vec<String>,
379 pub url_char_limit: usize,
382 pub title_char_limit: usize,
386 pub description_char_limit: usize,
390}
391
392impl Default for GitLabERGlobalSettings {
393 fn default() -> Self {
394 Self {
395 domain: "gitlab.com".to_owned(),
396 project_path: "<not set>".to_owned(),
397 labels: vec!["Auto Generated Issue".to_owned()],
398 url_char_limit: 2085,
399 title_char_limit: 1024,
400 description_char_limit: 1_048_576, }
402 }
403}
404
405#[derive(thiserror::Error, Debug, Clone)]
406enum GlobalSettingsError {
407 #[error("GitLab Global Settings where already set, this can only be set once.")]
408 AlreadySet,
409 #[error("GitLab Global Settings where not set.")]
410 SettingNotYetSet,
411 #[error("Could not acquire read lock to read GitLab Global Settings.")]
412 AcquireReadLockFailed,
413}