Skip to content

Commit b3900a0

Browse files
address some pr comments
1 parent 7a05e33 commit b3900a0

File tree

4 files changed

+313
-40
lines changed

4 files changed

+313
-40
lines changed

crates/common/src/integrations/gpt.rs

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ impl GptIntegration {
108108
}
109109
}
110110

111+
/// Build the upstream URL for a proxied GPT request.
112+
///
113+
/// Strips the integration prefix from the request path and constructs
114+
/// a full URL on the GPT host, preserving the original path and query.
115+
///
116+
/// Returns `None` if `request_path` does not start with [`ROUTE_PREFIX`].
117+
fn build_upstream_url(request_path: &str, query: Option<&str>) -> Option<String> {
118+
let upstream_path = request_path.strip_prefix(ROUTE_PREFIX)?;
119+
let query_part = query.map(|q| format!("?{}", q)).unwrap_or_default();
120+
Some(format!(
121+
"https://{SECUREPUBADS_HOST}{upstream_path}{query_part}"
122+
))
123+
}
124+
111125
/// Check if a URL points at Google's GPT bootstrap script (`gpt.js`).
112126
///
113127
/// Only matches the canonical host:
@@ -184,13 +198,9 @@ impl GptIntegration {
184198
)
185199
.with_header(
186200
header::CACHE_CONTROL,
187-
format!(
188-
"public, max-age={}, immutable",
189-
self.config.cache_ttl_seconds
190-
),
201+
format!("public, max-age={}", self.config.cache_ttl_seconds),
191202
)
192203
.with_header("X-GPT-Proxy", "true")
193-
.with_header("X-Script-Source", script_url)
194204
.with_body(body);
195205

196206
Self::copy_content_encoding_headers(&gpt_response, &mut response);
@@ -210,20 +220,11 @@ impl GptIntegration {
210220
req: Request,
211221
) -> Result<Response, Report<TrustedServerError>> {
212222
let original_path = req.get_path();
223+
let query = req.get_url().query();
213224

214-
// Strip the integration prefix to recover the upstream path.
215-
let upstream_path = original_path
216-
.strip_prefix(ROUTE_PREFIX)
225+
let target_url = Self::build_upstream_url(original_path, query)
217226
.ok_or_else(|| Self::error(format!("Invalid GPT pagead path: {}", original_path)))?;
218227

219-
let query = req
220-
.get_url()
221-
.query()
222-
.map(|q| format!("?{}", q))
223-
.unwrap_or_default();
224-
225-
let target_url = format!("https://{SECUREPUBADS_HOST}{upstream_path}{query}");
226-
227228
log::info!("GPT proxy: forwarding to {}", target_url);
228229

229230
let mut upstream_req = Request::new(Method::GET, &target_url);
@@ -269,13 +270,9 @@ impl GptIntegration {
269270
.with_header(header::CONTENT_TYPE, &content_type)
270271
.with_header(
271272
header::CACHE_CONTROL,
272-
format!(
273-
"public, max-age={}, immutable",
274-
self.config.cache_ttl_seconds
275-
),
273+
format!("public, max-age={}", self.config.cache_ttl_seconds),
276274
)
277275
.with_header("X-GPT-Proxy", "true")
278-
.with_header("X-Script-Source", &target_url)
279276
.with_body(body);
280277

281278
Self::copy_content_encoding_headers(&upstream_response, &mut response);
@@ -772,4 +769,109 @@ mod tests {
772769
"should not build when integration is disabled"
773770
);
774771
}
772+
773+
// -- Upstream URL building --
774+
775+
#[test]
776+
fn build_upstream_url_strips_prefix_and_preserves_path() {
777+
let url = GptIntegration::build_upstream_url(
778+
"/integrations/gpt/pagead/managed/js/gpt/current/pubads_impl.js",
779+
None,
780+
);
781+
assert_eq!(
782+
url.as_deref(),
783+
Some("https://securepubads.g.doubleclick.net/pagead/managed/js/gpt/current/pubads_impl.js"),
784+
"should strip the integration prefix and build the upstream URL"
785+
);
786+
}
787+
788+
#[test]
789+
fn build_upstream_url_preserves_query_string() {
790+
let url = GptIntegration::build_upstream_url(
791+
"/integrations/gpt/pagead/managed/js/gpt/current/pubads_impl.js",
792+
Some("cb=123&foo=bar"),
793+
);
794+
assert_eq!(
795+
url.as_deref(),
796+
Some("https://securepubads.g.doubleclick.net/pagead/managed/js/gpt/current/pubads_impl.js?cb=123&foo=bar"),
797+
"should preserve the query string in the upstream URL"
798+
);
799+
}
800+
801+
#[test]
802+
fn build_upstream_url_handles_tag_routes() {
803+
let url =
804+
GptIntegration::build_upstream_url("/integrations/gpt/tag/js/gpt.js", Some("v=2"));
805+
assert_eq!(
806+
url.as_deref(),
807+
Some("https://securepubads.g.doubleclick.net/tag/js/gpt.js?v=2"),
808+
"should handle /tag/* routes correctly"
809+
);
810+
}
811+
812+
#[test]
813+
fn build_upstream_url_returns_none_for_invalid_prefix() {
814+
let url = GptIntegration::build_upstream_url("/some/other/path", None);
815+
assert!(
816+
url.is_none(),
817+
"should return None when path does not start with the integration prefix"
818+
);
819+
}
820+
821+
#[test]
822+
fn build_upstream_url_handles_empty_path_after_prefix() {
823+
let url = GptIntegration::build_upstream_url("/integrations/gpt", None);
824+
assert_eq!(
825+
url.as_deref(),
826+
Some("https://securepubads.g.doubleclick.net"),
827+
"should handle path that is exactly the prefix"
828+
);
829+
}
830+
831+
// -- Vary header edge cases --
832+
833+
#[test]
834+
fn vary_with_accept_encoding_wildcard() {
835+
let result = GptIntegration::vary_with_accept_encoding(Some("*"));
836+
assert_eq!(
837+
result, "*",
838+
"should preserve Vary: * wildcard without appending Accept-Encoding"
839+
);
840+
}
841+
842+
#[test]
843+
fn vary_with_accept_encoding_case_insensitive() {
844+
let result = GptIntegration::vary_with_accept_encoding(Some("Origin, ACCEPT-ENCODING"));
845+
assert_eq!(
846+
result, "Origin, ACCEPT-ENCODING",
847+
"should detect Accept-Encoding case-insensitively"
848+
);
849+
}
850+
851+
#[test]
852+
fn vary_with_accept_encoding_adds_when_missing() {
853+
let result = GptIntegration::vary_with_accept_encoding(Some("Origin"));
854+
assert_eq!(
855+
result, "Origin, Accept-Encoding",
856+
"should append Accept-Encoding when not present"
857+
);
858+
}
859+
860+
#[test]
861+
fn vary_with_accept_encoding_empty_upstream() {
862+
let result = GptIntegration::vary_with_accept_encoding(None);
863+
assert_eq!(
864+
result, "Accept-Encoding",
865+
"should use Accept-Encoding as default when upstream has no Vary"
866+
);
867+
}
868+
869+
#[test]
870+
fn vary_with_accept_encoding_empty_string() {
871+
let result = GptIntegration::vary_with_accept_encoding(Some(""));
872+
assert_eq!(
873+
result, "Accept-Encoding",
874+
"should treat empty string the same as absent Vary"
875+
);
876+
}
775877
}

crates/js/lib/package-lock.json

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/js/lib/src/integrations/gpt/index.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,26 +95,46 @@ function wrapCommand(fn: () => void): () => void {
9595
/**
9696
* Patch `googletag.cmd` so every pushed callback runs through [`wrapCommand`].
9797
*
98-
* Any commands already queued before the shim loads are re-queued through the
99-
* wrapper so GPT executes them later via its normal queue-drain behavior.
98+
* Preserves the existing `tag.cmd` array identity so that GPT's own custom
99+
* `cmd.push` behaviour (immediate execution when the library is already
100+
* loaded) is not lost. The original `push` is saved and delegated to after
101+
* wrapping each callback.
102+
*
103+
* Already-queued callbacks are re-wrapped in place so GPT processes them
104+
* through our wrapper when it drains the queue.
100105
*/
101106
function patchCommandQueue(tag: Partial<GoogleTag>): void {
102-
const pending = Array.isArray(tag.cmd) ? [...tag.cmd] : [];
103-
const queue: Array<() => void> = [];
104-
tag.cmd = queue;
107+
// Ensure the queue exists.
108+
if (!Array.isArray(tag.cmd)) {
109+
tag.cmd = [];
110+
}
111+
112+
const queue = tag.cmd;
113+
114+
// Guard against double-patching (idempotent install).
115+
if ((queue as { __tsPushed?: boolean }).__tsPushed) {
116+
log.debug('GPT shim: command queue already patched, skipping');
117+
return;
118+
}
105119

106120
const originalPush = queue.push.bind(queue);
121+
122+
// Override push on the *existing* array — preserves object identity so
123+
// GPT (if already loaded) keeps its reference.
107124
queue.push = function (...callbacks: Array<() => void>): number {
108125
const wrapped = callbacks.map(wrapCommand);
109126
return originalPush(...wrapped);
110127
};
111128

112-
// Flush any commands that were queued before we took over
113-
if (pending.length > 0) {
114-
queue.push(...pending);
129+
// Mark as patched to prevent double-wrapping.
130+
(queue as { __tsPushed?: boolean }).__tsPushed = true;
131+
132+
// Re-wrap any callbacks that were queued before we patched.
133+
for (let i = 0; i < queue.length; i++) {
134+
queue[i] = wrapCommand(queue[i]);
115135
}
116136

117-
log.debug('GPT shim: command queue patched', { pendingCommands: pending.length });
137+
log.debug('GPT shim: command queue patched', { pendingCommands: queue.length });
118138
}
119139

120140
/**

0 commit comments

Comments
 (0)