diff --git a/services/reports/src/http/handlers.rs b/services/reports/src/http/handlers.rs --- a/services/reports/src/http/handlers.rs +++ b/services/reports/src/http/handlers.rs @@ -1,4 +1,5 @@ use actix_web::{get, post, web, HttpResponse}; +use http::header; use serde::Deserialize; use super::NotFoundHandler; @@ -49,3 +50,23 @@ let response = HttpResponse::Ok().json(report); Ok(response) } + +#[get("/{report_id}/redux-devtools.json")] +async fn redux_devtools_import( + path: web::Path, + service: ReportsService, +) -> actix_web::Result { + let report_id = path.into_inner(); + let devtools_json = service + .get_redux_devtools_import(report_id.clone().into()) + .await? + .unwrap_or_404()?; + + let response = HttpResponse::Ok() + .insert_header(( + header::CONTENT_DISPOSITION, + format!("attachment; filename=report-{}.json", report_id), + )) + .json(devtools_json); + Ok(response) +} diff --git a/services/reports/src/http/mod.rs b/services/reports/src/http/mod.rs --- a/services/reports/src/http/mod.rs +++ b/services/reports/src/http/mod.rs @@ -37,7 +37,8 @@ .service( web::scope("/reports") .service(handlers::post_reports) - .service(handlers::get_single_report), + .service(handlers::get_single_report) + .service(handlers::redux_devtools_import), ) }) .bind(("0.0.0.0", CONFIG.http_port))? diff --git a/services/reports/src/service.rs b/services/reports/src/service.rs --- a/services/reports/src/service.rs +++ b/services/reports/src/service.rs @@ -5,12 +5,15 @@ database, }; use derive_more::{Display, Error, From}; -use std::future::{ready, Ready}; +use std::{ + collections::HashMap, + future::{ready, Ready}, +}; use tracing::error; use crate::{ database::{client::DatabaseClient, item::ReportItem}, - report_types::{ReportID, ReportInput, ReportOutput}, + report_types::{ReportID, ReportInput, ReportOutput, ReportType}, }; #[derive(Debug, Display, Error, From)] @@ -125,6 +128,22 @@ }; Ok(Some(output)) } + + pub async fn get_redux_devtools_import( + &self, + report_id: ReportID, + ) -> ServiceResult> { + let Some(report) = self.get_report(report_id).await? else { + return Ok(None); + }; + if !matches!(report.report_type, ReportType::ErrorReport) { + return Err(ReportsServiceError::UnsupportedReportType); + }; + + let redux_devtools_payload = prepare_redux_devtools_import(report.content) + .map_err(ReportsServiceError::SerdeError)?; + Ok(Some(redux_devtools_payload)) + } } impl FromRequest for ReportsService { @@ -160,3 +179,29 @@ ready(Ok(auth_service)) } } + +/// Transforms report content JSON into format that can be +/// imported into Redux DevTools. +fn prepare_redux_devtools_import( + mut error_report: HashMap, +) -> Result { + use serde_json::{json, map::Map, Value}; + + let nav_state = error_report.remove("navState"); + let actions = error_report.remove("actions"); + let mut preloaded_state = error_report + .remove("preloadedState") + .unwrap_or_else(|| Value::Object(Map::new())); + + preloaded_state["navState"] = nav_state.into(); + preloaded_state["frozen"] = true.into(); + preloaded_state["_persist"]["rehydrated"] = false.into(); + + let preload_state_str = serde_json::to_string(&preloaded_state)?; + let payload_str = serde_json::to_string(&actions)?; + + Ok(json!({ + "preloadedState": preload_state_str, + "payload": payload_str, + })) +}