Skip to content

Commit 751f13a

Browse files
wip - permutive
1 parent fa46599 commit 751f13a

File tree

15 files changed

+268
-519
lines changed

15 files changed

+268
-519
lines changed

crates/common/src/error.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ pub enum TrustedServerError {
5454
#[display("Permutive SDK error: {message}")]
5555
PermutiveSdk { message: String },
5656

57-
/// Permutive API proxy error.
58-
#[display("Permutive API error: {message}")]
59-
PermutiveApi { message: String },
60-
6157
/// Proxy error.
6258
#[display("Proxy error: {message}")]
6359
Proxy { message: String },
@@ -100,7 +96,6 @@ impl IntoHttpResponse for TrustedServerError {
10096
Self::KvStore { .. } => StatusCode::SERVICE_UNAVAILABLE,
10197
Self::Prebid { .. } => StatusCode::BAD_GATEWAY,
10298
Self::PermutiveSdk { .. } => StatusCode::BAD_GATEWAY,
103-
Self::PermutiveApi { .. } => StatusCode::BAD_GATEWAY,
10499
Self::Proxy { .. } => StatusCode::BAD_GATEWAY,
105100
Self::SyntheticId { .. } => StatusCode::INTERNAL_SERVER_ERROR,
106101
Self::Template { .. } => StatusCode::INTERNAL_SERVER_ERROR,

crates/common/src/generic_proxy.rs

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,15 @@ pub async fn handle_generic_proxy(
6666
);
6767

6868
// Extract target path
69-
let target_path = mapping
70-
.extract_target_path(path)
71-
.ok_or_else(|| TrustedServerError::Proxy {
72-
message: format!(
73-
"Failed to extract target path from {} with prefix {}",
74-
path, mapping.prefix
75-
),
76-
})?;
69+
let target_path =
70+
mapping
71+
.extract_target_path(path)
72+
.ok_or_else(|| TrustedServerError::Proxy {
73+
message: format!(
74+
"Failed to extract target path from {} with prefix {}",
75+
path, mapping.prefix
76+
),
77+
})?;
7778

7879
// Build full target URL with query parameters
7980
let target_url = build_target_url(&mapping.target, target_path, &req)?;
@@ -95,11 +96,12 @@ pub async fn handle_generic_proxy(
9596
// Get backend and forward request
9697
let backend_name = ensure_backend_from_url(&mapping.target)?;
9798

98-
let target_response = target_req
99-
.send(backend_name)
100-
.change_context(TrustedServerError::Proxy {
101-
message: format!("Failed to forward request to {}", target_url),
102-
})?;
99+
let target_response =
100+
target_req
101+
.send(backend_name)
102+
.change_context(TrustedServerError::Proxy {
103+
message: format!("Failed to forward request to {}", target_url),
104+
})?;
103105

104106
log::info!(
105107
"Target responded with status: {}",
@@ -119,9 +121,7 @@ fn find_proxy_mapping<'a>(
119121
settings
120122
.proxy_mappings
121123
.iter()
122-
.find(|mapping| {
123-
mapping.matches_path(path) && mapping.supports_method(method.as_str())
124-
})
124+
.find(|mapping| mapping.matches_path(path) && mapping.supports_method(method.as_str()))
125125
.ok_or_else(|| {
126126
TrustedServerError::Proxy {
127127
message: format!(
@@ -252,10 +252,7 @@ mod tests {
252252
Some("/v2/projects")
253253
);
254254
assert_eq!(mapping.extract_target_path("/permutive/api"), Some(""));
255-
assert_eq!(
256-
mapping.extract_target_path("/permutive/api/"),
257-
Some("/")
258-
);
255+
assert_eq!(mapping.extract_target_path("/permutive/api/"), Some("/"));
259256
assert_eq!(mapping.extract_target_path("/other/path"), None);
260257
}
261258

crates/common/src/html_processor.rs

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
7676
});
7777

7878
let injected_tsjs = Rc::new(Cell::new(false));
79+
let injected_permutive_shim = Rc::new(Cell::new(false));
7980

8081
fn is_prebid_script_url(url: &str) -> bool {
8182
let lower = url.to_ascii_lowercase();
@@ -114,15 +115,23 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
114115
let patterns = patterns.clone();
115116
let rewrite_prebid = config.enable_prebid;
116117
let rewrite_permutive = config.enable_permutive;
118+
let injected_permutive_shim = injected_permutive_shim.clone();
117119
move |el| {
118120
if let Some(href) = el.get_attribute("href") {
119121
// If Prebid auto-config is enabled and this looks like a Prebid script href, rewrite to our extension
120122
if rewrite_prebid && is_prebid_script_url(&href) {
121123
let ext_src = tsjs::ext_script_src();
122124
el.set_attribute("href", &ext_src)?;
123125
} else if rewrite_permutive && is_permutive_sdk_url(&href) {
124-
let permutive_src = tsjs::permutive_script_src();
125-
el.set_attribute("href", &permutive_src)?;
126+
// Rewrite SDK URL to load through our proxy
127+
let sdk_src = tsjs::permutive_sdk_script_src();
128+
el.set_attribute("href", &sdk_src)?;
129+
// Also inject our shim script after the SDK
130+
if !injected_permutive_shim.get() {
131+
let shim_tag = tsjs::permutive_shim_script_tag();
132+
el.after(&shim_tag, ContentType::Html);
133+
injected_permutive_shim.set(true);
134+
}
126135
} else {
127136
let new_href = href
128137
.replace(&patterns.https_origin(), &patterns.replacement_url())
@@ -140,14 +149,22 @@ pub fn create_html_processor(config: HtmlProcessorConfig) -> impl StreamProcesso
140149
let patterns = patterns.clone();
141150
let rewrite_prebid = config.enable_prebid;
142151
let rewrite_permutive = config.enable_permutive;
152+
let injected_permutive_shim = injected_permutive_shim.clone();
143153
move |el| {
144154
if let Some(src) = el.get_attribute("src") {
145155
if rewrite_prebid && is_prebid_script_url(&src) {
146156
let ext_src = tsjs::ext_script_src();
147157
el.set_attribute("src", &ext_src)?;
148158
} else if rewrite_permutive && is_permutive_sdk_url(&src) {
149-
let permutive_src = tsjs::permutive_script_src();
150-
el.set_attribute("src", &permutive_src)?;
159+
// Rewrite SDK URL to load through our proxy
160+
let sdk_src = tsjs::permutive_sdk_script_src();
161+
el.set_attribute("src", &sdk_src)?;
162+
// Also inject our shim script after the SDK
163+
if !injected_permutive_shim.get() {
164+
let shim_tag = tsjs::permutive_shim_script_tag();
165+
el.after(&shim_tag, ContentType::Html);
166+
injected_permutive_shim.set(true);
167+
}
151168
} else {
152169
let new_src = src
153170
.replace(&patterns.https_origin(), &patterns.replacement_url())
@@ -344,7 +361,7 @@ mod tests {
344361
</head><body></body></html>"#;
345362

346363
let mut config = create_test_config();
347-
config.enable_permutive = true; // enable rewriting of Permutive URLs
364+
config.enable_permutive = true; // enable Permutive SDK rewriting and shim injection
348365
let processor = create_html_processor(config);
349366
let pipeline_config = PipelineConfig {
350367
input_compression: Compression::None,
@@ -358,9 +375,15 @@ mod tests {
358375
assert!(result.is_ok());
359376
let processed = String::from_utf8_lossy(&output);
360377
assert!(processed.contains("/static/tsjs=tsjs-core.min.js"));
361-
// Permutive references are rewritten to our permutive bundle when auto-configure is on
362-
assert!(processed.contains("/static/tsjs=tsjs-permutive.min.js"));
378+
// Permutive SDK URL should be rewritten to first-party proxy
379+
assert!(processed.contains("/static/tsjs=tsjs-permutive-sdk.js"));
363380
assert!(!processed.contains("edge.permutive.app"));
381+
// Shim script should be injected after the SDK
382+
assert!(processed.contains("/static/tsjs=tsjs-permutive-shim.min.js"));
383+
// Verify shim comes after SDK
384+
let sdk_pos = processed.find("tsjs-permutive-sdk.js").unwrap();
385+
let shim_pos = processed.find("tsjs-permutive-shim.min.js").unwrap();
386+
assert!(shim_pos > sdk_pos, "Shim should be injected after SDK");
364387
}
365388

366389
#[test]
@@ -383,8 +406,11 @@ mod tests {
383406
let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output);
384407
assert!(result.is_ok());
385408
let processed = String::from_utf8_lossy(&output);
386-
assert!(processed.contains("/static/tsjs=tsjs-permutive.min.js"));
409+
// Permutive SDK URL should be rewritten to first-party proxy
410+
assert!(processed.contains("/static/tsjs=tsjs-permutive-sdk.js"));
387411
assert!(!processed.contains("cdn.permutive.com"));
412+
// Shim should be injected
413+
assert!(processed.contains("/static/tsjs=tsjs-permutive-shim.min.js"));
388414
}
389415

390416
#[test]
@@ -412,6 +438,96 @@ mod tests {
412438
assert!(!processed.contains("tsjs-permutive"));
413439
}
414440

441+
#[test]
442+
fn test_permutive_sdk_injection_rewrites_and_adds_shim() {
443+
let html = r#"<html>
444+
<head>
445+
<script src="https://workspace-id.edge.permutive.app/org-id-web.js"></script>
446+
</head>
447+
<body></body>
448+
</html>"#;
449+
450+
let mut config = create_test_config();
451+
config.enable_permutive = true;
452+
let processor = create_html_processor(config);
453+
let pipeline_config = PipelineConfig {
454+
input_compression: Compression::None,
455+
output_compression: Compression::None,
456+
chunk_size: 8192,
457+
};
458+
let mut pipeline = StreamingPipeline::new(pipeline_config, processor);
459+
460+
let mut output = Vec::new();
461+
let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output);
462+
assert!(result.is_ok());
463+
let processed = String::from_utf8_lossy(&output);
464+
465+
// SDK URL should be rewritten to first-party proxy
466+
assert!(
467+
processed.contains("/static/tsjs=tsjs-permutive-sdk.js"),
468+
"SDK URL should be rewritten to first-party"
469+
);
470+
assert!(
471+
!processed.contains("edge.permutive.app"),
472+
"Original SDK domain should not be present"
473+
);
474+
475+
// Should add our shim script after it
476+
assert!(
477+
processed.contains("/static/tsjs=tsjs-permutive-shim.min.js"),
478+
"Shim script should be injected"
479+
);
480+
481+
// Shim should come after SDK
482+
let sdk_pos = processed.find("tsjs-permutive-sdk.js").unwrap();
483+
let shim_pos = processed.find("tsjs-permutive-shim.min.js").unwrap();
484+
assert!(
485+
shim_pos > sdk_pos,
486+
"Shim should be injected after SDK script"
487+
);
488+
}
489+
490+
#[test]
491+
fn test_permutive_multiple_sdks_only_injects_once() {
492+
let html = r#"<html>
493+
<head>
494+
<script src="https://workspace1.edge.permutive.app/org1-web.js"></script>
495+
<link rel="preload" as="script" href="https://workspace2.edge.permutive.app/org2-web.js" />
496+
<script src="https://cdn.permutive.com/workspace3-web.js"></script>
497+
</head>
498+
<body></body>
499+
</html>"#;
500+
501+
let mut config = create_test_config();
502+
config.enable_permutive = true;
503+
let processor = create_html_processor(config);
504+
let pipeline_config = PipelineConfig {
505+
input_compression: Compression::None,
506+
output_compression: Compression::None,
507+
chunk_size: 8192,
508+
};
509+
let mut pipeline = StreamingPipeline::new(pipeline_config, processor);
510+
511+
let mut output = Vec::new();
512+
let result = pipeline.process(Cursor::new(html.as_bytes()), &mut output);
513+
assert!(result.is_ok());
514+
let processed = String::from_utf8_lossy(&output);
515+
516+
// All SDK URLs should be rewritten to first-party
517+
assert!(processed.contains("/static/tsjs=tsjs-permutive-sdk.js"));
518+
assert!(!processed.contains("workspace1.edge.permutive.app"));
519+
assert!(!processed.contains("workspace2.edge.permutive.app"));
520+
assert!(!processed.contains("cdn.permutive.com"));
521+
522+
// Shim should only be injected once
523+
let shim_count = processed.matches("tsjs-permutive-shim.min.js").count();
524+
assert_eq!(
525+
shim_count, 1,
526+
"Shim script should only be injected once, found {} times",
527+
shim_count
528+
);
529+
}
530+
415531
#[test]
416532
fn test_create_html_processor_url_replacement() {
417533
let config = create_test_config();

crates/common/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ pub mod html_processor;
3636
pub mod http_util;
3737
pub mod models;
3838
pub mod openrtb;
39-
pub mod permutive_proxy;
4039
pub mod permutive_sdk;
4140
pub mod prebid_proxy;
4241
pub mod proxy;

0 commit comments

Comments
 (0)