Skip to content

Commit f33e96c

Browse files
authored
Merge pull request #32 from faststats-dev/refactor/breaking-changes
refactor: unify sourcemap backend across different types
2 parents 5f25563 + e1f7588 commit f33e96c

File tree

5 files changed

+137
-242
lines changed

5 files changed

+137
-242
lines changed

.changeset/busy-experts-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@faststats/sourcemap-uploader-plugin": minor
3+
---
4+
5+
refactor: use new api routes

apps/backend/src/routes.rs

Lines changed: 48 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,26 @@ use crate::storage::StoredObjectMeta;
1919
const INGEST_MAX_BODY_BYTES: usize = 50 * 1024 * 1024;
2020

2121
#[derive(Debug, Deserialize)]
22-
pub struct SourcemapEntry {
23-
#[serde(alias = "fileName")]
24-
pub file_name: String,
25-
pub sourcemap: String,
26-
}
27-
28-
#[derive(Debug, Deserialize)]
29-
pub struct ProguardEntry {
30-
#[serde(alias = "fileName")]
22+
#[serde(rename_all = "camelCase")]
23+
pub struct UploadFile {
3124
pub file_name: String,
32-
#[serde(alias = "content")]
33-
pub mapping: String,
25+
pub content: String,
3426
}
3527

3628
#[derive(Debug, Deserialize)]
37-
#[serde(tag = "mappingType")]
29+
#[serde(tag = "type")]
3830
pub enum IngestPayload {
3931
#[serde(rename = "javascript")]
4032
JavaScript {
41-
#[serde(alias = "buildId")]
4233
build_id: String,
43-
bundler: String,
44-
#[serde(alias = "uploadedAt")]
4534
uploaded_at: String,
46-
sourcemaps: Vec<SourcemapEntry>,
35+
files: Vec<UploadFile>,
4736
},
4837
#[serde(rename = "proguard")]
4938
Proguard {
50-
#[serde(alias = "buildId")]
5139
build_id: String,
52-
#[serde(alias = "uploadedAt")]
5340
uploaded_at: String,
54-
#[serde(alias = "fileName")]
55-
file_name: Option<String>,
56-
mapping: Option<String>,
57-
#[serde(default)]
58-
mappings: Vec<ProguardEntry>,
59-
#[serde(default)]
60-
files: Vec<ProguardEntry>,
41+
files: Vec<UploadFile>,
6142
},
6243
}
6344

@@ -157,7 +138,7 @@ pub fn public_router(state: SharedState) -> Router {
157138
Router::new()
158139
.route("/health", get(health))
159140
.route(
160-
"/api/sourcemaps",
141+
"/v0/upload",
161142
post(ingest).route_layer(DefaultBodyLimit::max(INGEST_MAX_BODY_BYTES)),
162143
)
163144
.layer(TraceLayer::new_for_http())
@@ -190,49 +171,36 @@ pub async fn ingest(
190171
let (build_id, uploaded_at, ingested, total_bytes, mapping_type) = match &payload {
191172
IngestPayload::JavaScript {
192173
build_id,
193-
bundler,
194174
uploaded_at,
195-
sourcemaps,
175+
files,
196176
} => {
197-
validate_js_ingest(build_id, uploaded_at, sourcemaps)?;
198-
let entries: Vec<(String, String)> = sourcemaps
199-
.iter()
200-
.map(|e| (e.file_name.clone(), e.sourcemap.clone()))
201-
.collect();
177+
let entries = normalize_upload_files(build_id, uploaded_at, files)?;
202178
crate::mappings::javascript::ingest(&state.storage, project_id, build_id, &entries)
203179
.await?;
204-
let total_bytes: usize = sourcemaps.iter().map(|e| e.sourcemap.len()).sum();
205-
let file_names: Vec<&str> = sourcemaps.iter().map(|e| e.file_name.as_str()).collect();
180+
let total_bytes: usize = entries.iter().map(|(_, content)| content.len()).sum();
181+
let file_names: Vec<&str> = entries
182+
.iter()
183+
.map(|(file_name, _)| file_name.as_str())
184+
.collect();
206185
info!(
207186
%project_id,
208187
build_id,
209-
%bundler,
210188
files = ?file_names,
211189
);
212190
(
213191
build_id.as_str(),
214192
uploaded_at.as_str(),
215-
sourcemaps.len(),
193+
entries.len(),
216194
total_bytes,
217195
"javascript",
218196
)
219197
}
220198
IngestPayload::Proguard {
221199
build_id,
222200
uploaded_at,
223-
mapping,
224-
file_name,
225-
mappings,
226201
files,
227202
} => {
228-
let mappings = normalize_proguard_entries(
229-
build_id,
230-
uploaded_at,
231-
file_name,
232-
mapping,
233-
mappings,
234-
files,
235-
)?;
203+
let mappings = normalize_upload_files(build_id, uploaded_at, files)?;
236204
crate::mappings::proguard::ingest(&state.storage, project_id, build_id, &mappings)
237205
.await?;
238206
let total_bytes: usize = mappings.iter().map(|(_, mapping)| mapping.len()).sum();
@@ -463,68 +431,23 @@ pub async fn cleanup_old_builds(
463431
}))
464432
}
465433

466-
fn validate_js_ingest(
434+
fn normalize_upload_files(
467435
build_id: &str,
468436
uploaded_at: &str,
469-
sourcemaps: &[SourcemapEntry],
470-
) -> Result<(), AppError> {
471-
require_non_empty("build_id", build_id)?;
472-
require_non_empty("uploaded_at", uploaded_at)?;
473-
if sourcemaps.is_empty() {
474-
return Err(AppError::BadRequest("no sourcemaps provided".into()));
475-
}
476-
for entry in sourcemaps {
477-
require_non_empty("file_name", &entry.file_name)?;
478-
require_non_empty("sourcemap", &entry.sourcemap)?;
479-
}
480-
Ok(())
481-
}
482-
483-
fn normalize_proguard_entries(
484-
build_id: &str,
485-
uploaded_at: &str,
486-
file_name: &Option<String>,
487-
mapping: &Option<String>,
488-
mappings: &[ProguardEntry],
489-
files: &[ProguardEntry],
437+
files: &[UploadFile],
490438
) -> Result<Vec<(String, String)>, AppError> {
491439
require_non_empty("build_id", build_id)?;
492440
require_non_empty("uploaded_at", uploaded_at)?;
493-
494-
let mut normalized =
495-
Vec::with_capacity(mappings.len() + files.len() + usize::from(mapping.is_some()));
496-
normalized.extend(
497-
mappings
498-
.iter()
499-
.map(|entry| (entry.file_name.clone(), entry.mapping.clone())),
500-
);
501-
normalized.extend(
502-
files
503-
.iter()
504-
.map(|entry| (entry.file_name.clone(), entry.mapping.clone())),
505-
);
506-
507-
match (file_name.as_deref(), mapping.as_deref()) {
508-
(Some(file_name), Some(mapping)) => {
509-
normalized.push((file_name.to_string(), mapping.to_string()));
510-
}
511-
(Some(_), None) => return Err(AppError::BadRequest("mapping is required".into())),
512-
(None, Some(mapping)) if normalized.is_empty() => {
513-
normalized.push(("mapping.txt".to_string(), mapping.to_string()));
514-
}
515-
(None, Some(_)) => return Err(AppError::BadRequest("file_name is required".into())),
516-
(None, None) => {}
517-
}
518-
519-
if normalized.is_empty() {
520-
return Err(AppError::BadRequest("no proguard mappings provided".into()));
441+
if files.is_empty() {
442+
return Err(AppError::BadRequest("no files provided".into()));
521443
}
522444

523-
for (file_name, mapping) in &normalized {
524-
require_non_empty("file_name", file_name)?;
525-
require_non_empty("mapping", mapping)?;
445+
let mut normalized = Vec::with_capacity(files.len());
446+
for entry in files {
447+
require_non_empty("file_name", &entry.file_name)?;
448+
require_non_empty("content", &entry.content)?;
449+
normalized.push((entry.file_name.clone(), entry.content.clone()));
526450
}
527-
528451
Ok(normalized)
529452
}
530453

@@ -600,7 +523,7 @@ fn select_builds_for_cleanup(
600523
#[cfg(test)]
601524
mod tests {
602525
use super::{
603-
ProguardEntry, normalize_proguard_entries, normalized_build_ids, parse_sourcemap_key,
526+
UploadFile, normalize_upload_files, normalized_build_ids, parse_sourcemap_key,
604527
select_builds_for_cleanup, validate_project_sourcemap_key,
605528
};
606529
use crate::mappings::{javascript::map_file_name, require_non_empty};
@@ -674,20 +597,20 @@ mod tests {
674597
}
675598

676599
#[test]
677-
fn normalize_proguard_entries_accepts_multiple_named_files() {
678-
let normalized = normalize_proguard_entries(
600+
fn normalize_upload_files_accepts_multiple_named_files() {
601+
let normalized = normalize_upload_files(
679602
"build-1",
680603
"2026-03-22T00:00:00Z",
681-
&None,
682-
&None,
683-
&[ProguardEntry {
684-
file_name: "base.txt".to_string(),
685-
mapping: "one".to_string(),
686-
}],
687-
&[ProguardEntry {
688-
file_name: "feature.txt".to_string(),
689-
mapping: "two".to_string(),
690-
}],
604+
&[
605+
UploadFile {
606+
file_name: "base.txt".to_string(),
607+
content: "one".to_string(),
608+
},
609+
UploadFile {
610+
file_name: "feature.txt".to_string(),
611+
content: "two".to_string(),
612+
},
613+
],
691614
)
692615
.expect("entries should normalize");
693616

@@ -701,39 +624,26 @@ mod tests {
701624
}
702625

703626
#[test]
704-
fn normalize_proguard_entries_keeps_legacy_single_mapping_compatible() {
705-
let normalized = normalize_proguard_entries(
706-
"build-1",
707-
"2026-03-22T00:00:00Z",
708-
&None,
709-
&Some("contents".to_string()),
710-
&[],
711-
&[],
712-
)
713-
.expect("legacy payload should normalize");
627+
fn normalize_upload_files_rejects_empty_payload() {
628+
let err = normalize_upload_files("build-1", "2026-03-22T00:00:00Z", &[])
629+
.expect_err("empty payload should fail");
714630

715-
assert_eq!(
716-
normalized,
717-
vec![("mapping.txt".to_string(), "contents".to_string())]
718-
);
631+
assert!(format!("{err}").contains("no files provided"));
719632
}
720633

721634
#[test]
722-
fn normalize_proguard_entries_rejects_mixed_unnamed_mapping() {
723-
let err = normalize_proguard_entries(
635+
fn normalize_upload_files_rejects_empty_content() {
636+
let err = normalize_upload_files(
724637
"build-1",
725638
"2026-03-22T00:00:00Z",
726-
&None,
727-
&Some("contents".to_string()),
728-
&[ProguardEntry {
639+
&[UploadFile {
729640
file_name: "base.txt".to_string(),
730-
mapping: "one".to_string(),
641+
content: " ".to_string(),
731642
}],
732-
&[],
733643
)
734-
.expect_err("mixed unnamed mapping should be rejected");
644+
.expect_err("blank file content should be rejected");
735645

736-
assert!(format!("{err}").contains("file_name is required"));
646+
assert!(format!("{err}").contains("content is required"));
737647
}
738648

739649
#[test]

packages/bundler-plugin/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import sourcemapsPlugin from "@sourcemaps/bundler-plugin/vite";
3232
export default {
3333
plugins: [
3434
sourcemapsPlugin({
35-
endpoint: "http://localhost:3000/api/sourcemaps",
35+
endpoint: "http://localhost:3000/v0/upload",
3636
deleteAfterUpload: true,
3737
}),
3838
],
@@ -51,6 +51,24 @@ export default {
5151
- `onUploadSuccess`: callback after successful upload
5252
- `onUploadError`: callback when upload fails
5353

54+
## Upload payload
55+
56+
The plugin sends `POST /v0/upload` with this JSON body:
57+
58+
```json
59+
{
60+
"type": "javascript",
61+
"buildId": "build-123",
62+
"uploadedAt": "2026-04-01T12:00:00.000Z",
63+
"files": [
64+
{
65+
"fileName": "assets/app.js.map",
66+
"content": "{...}"
67+
}
68+
]
69+
}
70+
```
71+
5472
## Tests
5573

5674
```bash

0 commit comments

Comments
 (0)