Skip to content

Commit d54587c

Browse files
committed
feat(api, web-scraper): add support for debug endpoints
1 parent 0eacaab commit d54587c

22 files changed

+6282
-2258
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
6464
- name: Test (default features)
6565
timeout-minutes: 6
66-
run: cargo test -- --nocapture
66+
run: cargo test --all -- --nocapture
6767

6868
- name: Check database schema
6969
run: cargo sqlx prepare --check

components/retrack-types/src/trackers.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ mod tracker_config;
44
mod tracker_create_params;
55
mod tracker_data_revision;
66
mod tracker_data_value;
7+
mod tracker_debug_existing_params;
8+
mod tracker_debug_params;
9+
mod tracker_debug_result;
710
mod tracker_list_revisions_params;
811
mod tracker_target;
912
mod tracker_update_params;
@@ -19,6 +22,14 @@ pub use self::{
1922
tracker_create_params::TrackerCreateParams,
2023
tracker_data_revision::TrackerDataRevision,
2124
tracker_data_value::TrackerDataValue,
25+
tracker_debug_existing_params::TrackerDebugExistingParams,
26+
tracker_debug_params::TrackerDebugParams,
27+
tracker_debug_result::{
28+
ActionDebugInfo, ActionDestinationDebugInfo, ApiRequestDebugInfo, ApiTrackerDebugResult,
29+
AutoParseDebugInfo, EmailDestinationDebugInfo, PageLogEntry, PageTrackerDebugResult,
30+
RenderedEmailDebugInfo, ScriptDebugInfo, TrackerDebugResult, TrackerDebugTargetResult,
31+
WebhookDestinationDebugInfo,
32+
},
2233
tracker_list_revisions_params::{
2334
DEFAULT_DIFF_CONTEXT_RADIUS, MAX_DIFF_CONTEXT_RADIUS, TrackerListRevisionsParams,
2435
},
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use crate::trackers::{TrackerAction, TrackerConfig, TrackerDataValue, TrackerTarget};
2+
use serde::{Deserialize, Serialize};
3+
use serde_with::skip_serializing_none;
4+
use utoipa::ToSchema;
5+
6+
/// Parameters for the existing-tracker debug endpoint
7+
/// (`POST /api/trackers/{tracker_id}/_debug`).
8+
///
9+
/// Every field is optional. When provided, it overrides the corresponding field on the
10+
/// stored tracker for this debug run only (nothing is persisted).
11+
#[skip_serializing_none]
12+
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, ToSchema)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct TrackerDebugExistingParams {
15+
/// Override the tracker's target for this debug run.
16+
pub target: Option<TrackerTarget>,
17+
/// Override the tracker's config for this debug run.
18+
pub config: Option<TrackerConfig>,
19+
/// Override the tracker's tags for this debug run.
20+
pub tags: Option<Vec<String>>,
21+
/// Override the tracker's actions for this debug run.
22+
pub actions: Option<Vec<TrackerAction>>,
23+
/// Supply previous content instead of using the latest stored revision.
24+
pub previous_content: Option<TrackerDataValue>,
25+
}
26+
27+
#[cfg(test)]
28+
mod tests {
29+
use super::TrackerDebugExistingParams;
30+
use crate::trackers::{PageTarget, TrackerDataValue, TrackerTarget};
31+
use insta::assert_json_snapshot;
32+
use serde_json::json;
33+
34+
#[test]
35+
fn deserialization_empty() -> anyhow::Result<()> {
36+
let params: TrackerDebugExistingParams = serde_json::from_value(json!({}))?;
37+
assert_eq!(params, TrackerDebugExistingParams::default());
38+
Ok(())
39+
}
40+
41+
#[test]
42+
fn deserialization_with_overrides() -> anyhow::Result<()> {
43+
let params: TrackerDebugExistingParams = serde_json::from_value(json!({
44+
"target": {
45+
"type": "page",
46+
"extractor": "export async function execute(p) { return 'ok'; }"
47+
},
48+
"tags": ["override-tag"],
49+
"previousContent": { "original": "prev" }
50+
}))?;
51+
52+
assert!(params.target.is_some());
53+
assert!(params.config.is_none());
54+
assert_eq!(params.tags, Some(vec!["override-tag".to_string()]));
55+
assert!(params.actions.is_none());
56+
assert_eq!(
57+
params.previous_content,
58+
Some(TrackerDataValue::new(json!("prev")))
59+
);
60+
61+
Ok(())
62+
}
63+
64+
#[test]
65+
fn serialization_roundtrip() -> anyhow::Result<()> {
66+
let params = TrackerDebugExistingParams {
67+
target: Some(TrackerTarget::Page(PageTarget {
68+
extractor: "script".to_string(),
69+
params: None,
70+
engine: None,
71+
user_agent: None,
72+
accept_invalid_certificates: false,
73+
})),
74+
config: None,
75+
tags: Some(vec!["t".to_string()]),
76+
actions: None,
77+
previous_content: Some(TrackerDataValue::new(json!("prev"))),
78+
};
79+
80+
assert_json_snapshot!(params, @r###"
81+
{
82+
"target": {
83+
"type": "page",
84+
"extractor": "script"
85+
},
86+
"tags": [
87+
"t"
88+
],
89+
"previousContent": {
90+
"original": "prev"
91+
}
92+
}
93+
"###);
94+
95+
let roundtrip: TrackerDebugExistingParams =
96+
serde_json::from_value(serde_json::to_value(&params)?)?;
97+
assert_eq!(roundtrip, params);
98+
99+
Ok(())
100+
}
101+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use crate::trackers::{TrackerAction, TrackerConfig, TrackerDataValue, TrackerTarget};
2+
use serde::{Deserialize, Serialize};
3+
use serde_with::skip_serializing_none;
4+
use utoipa::ToSchema;
5+
6+
/// Parameters for the ad-hoc tracker debug endpoint (`POST /api/trackers/_debug`).
7+
///
8+
/// Accepts the same target/config shape as `TrackerCreateParams` but does not persist
9+
/// anything. Fields irrelevant to a debug run (schedule, revisions count) are accepted
10+
/// but ignored.
11+
#[skip_serializing_none]
12+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct TrackerDebugParams {
15+
/// Target of the tracker (web page or API).
16+
pub target: TrackerTarget,
17+
/// Tracker config (only `timeout` is meaningful for debug runs).
18+
#[serde(default)]
19+
pub config: TrackerConfig,
20+
/// Tracker tags.
21+
#[serde(default)]
22+
pub tags: Vec<String>,
23+
/// Actions to simulate (dry-run). No side-effects are produced.
24+
#[serde(default)]
25+
pub actions: Vec<TrackerAction>,
26+
/// Optional previous content to simulate how the tracker behaves with existing data.
27+
pub previous_content: Option<TrackerDataValue>,
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::TrackerDebugParams;
33+
use crate::trackers::{
34+
PageTarget, TrackerAction, TrackerConfig, TrackerDataValue, TrackerTarget, WebhookAction,
35+
};
36+
use insta::assert_json_snapshot;
37+
use serde_json::json;
38+
use std::time::Duration;
39+
40+
#[test]
41+
fn deserialization_minimal() -> anyhow::Result<()> {
42+
let params: TrackerDebugParams = serde_json::from_value(json!({
43+
"target": {
44+
"type": "page",
45+
"extractor": "export async function execute(p) { return 'ok'; }"
46+
}
47+
}))?;
48+
49+
assert_eq!(
50+
params.target,
51+
TrackerTarget::Page(PageTarget {
52+
extractor: "export async function execute(p) { return 'ok'; }".to_string(),
53+
params: None,
54+
engine: None,
55+
user_agent: None,
56+
accept_invalid_certificates: false,
57+
})
58+
);
59+
assert_eq!(params.config, TrackerConfig::default());
60+
assert!(params.tags.is_empty());
61+
assert!(params.actions.is_empty());
62+
assert!(params.previous_content.is_none());
63+
64+
Ok(())
65+
}
66+
67+
#[test]
68+
fn deserialization_full() -> anyhow::Result<()> {
69+
let params: TrackerDebugParams = serde_json::from_value(json!({
70+
"target": {
71+
"type": "page",
72+
"extractor": "export async function execute(p) { return 'ok'; }"
73+
},
74+
"config": { "revisions": 1, "timeout": 5000 },
75+
"tags": ["t1"],
76+
"actions": [{ "type": "webhook", "url": "https://retrack.dev/" }],
77+
"previousContent": { "original": "prev" }
78+
}))?;
79+
80+
assert_eq!(params.config.timeout, Some(Duration::from_millis(5000)));
81+
assert_eq!(params.tags, vec!["t1"]);
82+
assert_eq!(params.actions.len(), 1);
83+
assert_eq!(
84+
params.previous_content,
85+
Some(TrackerDataValue::new(json!("prev")))
86+
);
87+
88+
Ok(())
89+
}
90+
91+
#[test]
92+
fn serialization_roundtrip() -> anyhow::Result<()> {
93+
let params = TrackerDebugParams {
94+
target: TrackerTarget::Page(PageTarget {
95+
extractor: "script".to_string(),
96+
params: None,
97+
engine: None,
98+
user_agent: None,
99+
accept_invalid_certificates: false,
100+
}),
101+
config: TrackerConfig::default(),
102+
tags: vec!["tag".to_string()],
103+
actions: vec![TrackerAction::Webhook(WebhookAction {
104+
url: "https://retrack.dev".parse()?,
105+
method: None,
106+
headers: None,
107+
formatter: None,
108+
})],
109+
previous_content: Some(TrackerDataValue::new(json!("prev"))),
110+
};
111+
112+
assert_json_snapshot!(params, @r###"
113+
{
114+
"target": {
115+
"type": "page",
116+
"extractor": "script"
117+
},
118+
"config": {
119+
"revisions": 3
120+
},
121+
"tags": [
122+
"tag"
123+
],
124+
"actions": [
125+
{
126+
"type": "webhook",
127+
"url": "https://retrack.dev/"
128+
}
129+
],
130+
"previousContent": {
131+
"original": "prev"
132+
}
133+
}
134+
"###);
135+
136+
let roundtrip: TrackerDebugParams = serde_json::from_value(serde_json::to_value(&params)?)?;
137+
assert_eq!(roundtrip, params);
138+
139+
Ok(())
140+
}
141+
}

0 commit comments

Comments
 (0)