Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/base/test_cases/trace-context-callee/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Deno.serve((req: Request) =>
Response.json({
traceparent: req.headers.get("traceparent"),
tracestate: req.headers.get("tracestate"),
baggage: req.headers.get("baggage"),
custom: req.headers.get("x-app-traceparent"),
})
);
53 changes: 53 additions & 0 deletions crates/base/test_cases/trace-context-caller/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createClient } from "npm:@supabase/supabase-js@2.112.3";

const traceparent = "00-b1e669305668dc82c96cc31ce2e6cd99-1a5b74eca03f769f-01";

Deno.serve(async (req: Request) => {
const origin = new URL(req.url).origin;
const client = createClient(origin, "test-key", {
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
});

const results = await Promise.all(
["fetch", "request", "invoke"].flatMap((method) =>
["present", "empty", "absent"].map(async (state) => {
const headers: Record<string, string> = {
// Exercise case-insensitive detection and an unrelated header.
TraceParent: traceparent,
"x-app-traceparent": traceparent,
baggage: "caller=value",
};
if (state !== "absent") {
headers.TraceState = state === "empty" ? "" : "caller=value";
}

let data;
if (method === "invoke") {
const result = await client.functions.invoke("callee", {
headers,
body: { hello: "world" },
});
if (result.error) throw result.error;
data = result.data;
} else {
const url = `${origin}/callee`;
const response = method === "request"
? await fetch(new Request(url, { headers }))
: await fetch(url, { headers });
if (!response.ok) throw new Error(await response.text());
data = await response.json();
}
return { method, state, ...data };
})
),
);

// Automatic propagation must still work when no context is supplied.
const automatic = await fetch(`${origin}/callee`);
if (!automatic.ok) throw new Error(await automatic.text());
return Response.json({ results, automatic: await automatic.json() });
});
20 changes: 20 additions & 0 deletions crates/base/test_cases/trace-context-main/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Deno.serve(async (req: Request) => {
const name = new URL(req.url).pathname.split("/").pop();
const isCaller = name === "caller" || name === "caller-untraced";
if (!isCaller && name !== "callee") {
return new Response("Not found", { status: 404 });
}

const worker = await EdgeRuntime.userWorkers.create({
servicePath: `./test_cases/trace-context-${isCaller ? "caller" : "callee"}`,
memoryLimitMb: 150,
workerTimeoutMs: 60_000,
cpuTimeSoftLimitMs: 60_000,
cpuTimeHardLimitMs: 60_000,
otelConfig: {
tracing_enabled: name !== "caller-untraced",
propagators: ["TraceContext", "Baggage"],
},
});
return await worker.fetch(req);
});
81 changes: 81 additions & 0 deletions crates/base/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4271,6 +4271,87 @@ async fn test_brotli_async() {
);
}

async fn assert_fetch_trace_context(tracing_enabled: bool) {
init_otel();
let caller = if tracing_enabled {
"caller"
} else {
"caller-untraced"
};

integration_test_with_server_flag!(
ServerFlags::default(),
"./test_cases/trace-context-main",
NON_SECURE_PORT,
caller,
None,
Some(
reqwest::Client::new()
.get(format!("http://localhost:{NON_SECURE_PORT}/{caller}"))
.header(
"traceparent",
"00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01",
)
.header("tracestate", "upstream=value")
.header("baggage", "tenant=test")
),
None::<Tls>,
(|resp| async {
let resp = resp.unwrap();
let status = resp.status();
let body = resp.text().await.unwrap();
assert_eq!(status.as_u16(), 200, "{body}");
let data: serde_json::Value = serde_json::from_str(&body).unwrap();
let results = data["results"].as_array().unwrap();
assert_eq!(results.len(), 9);
for result in results {
assert_eq!(
result["traceparent"],
"00-b1e669305668dc82c96cc31ce2e6cd99-1a5b74eca03f769f-01",
"{result}",
);
assert_eq!(result["custom"], result["traceparent"]);
match result["state"].as_str().unwrap() {
"present" => assert_eq!(result["tracestate"], "caller=value"),
"empty" => assert_eq!(result["tracestate"], ""),
"absent" => assert!(result["tracestate"].is_null()),
state => panic!("unexpected state: {state}"),
}
assert_eq!(result["baggage"], "caller=value");
}
let automatic = &data["automatic"];
if tracing_enabled {
let traceparent = automatic["traceparent"].as_str().unwrap();
let parts: Vec<_> = traceparent.split('-').collect();
assert_eq!(parts.len(), 4);
assert_eq!(parts[0], "00");
assert_eq!(parts[1], "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
assert_eq!(parts[2].len(), 16);
assert!(parts[2].bytes().all(|byte| byte.is_ascii_hexdigit()));
assert_ne!(parts[2], "0000000000000000");
assert_eq!(parts[3], "01");
} else {
assert!(automatic["traceparent"].is_null());
assert!(automatic["tracestate"].is_null());
assert!(automatic["baggage"].is_null());
}
}),
TerminationToken::new()
);
}

#[tokio::test]
#[serial]
async fn test_fetch_preserves_explicit_trace_context() {
assert_fetch_trace_context(true).await;
}

#[tokio::test]
#[serial]
async fn test_fetch_preserves_explicit_trace_context_without_tracing() {
assert_fetch_trace_context(false).await;
}

async fn assert_rate_limit_error(
resp: Result<reqwest::Response, reqwest::Error>,
) {
Expand Down
9 changes: 9 additions & 0 deletions vendor/deno_fetch/26_fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,15 @@ function fetch(input, init = { __proto__: null }) {
if (span) {
const context = ContextManager.active();
for (const propagator of new SafeArrayIterator(PROPAGATORS)) {
// Explicit trace context belongs to the caller. Appending another
// traceparent makes it invalid, and injecting the active tracestate
// would associate state from a different trace with that context.
if (
requestObject.headers.has("traceparent") &&
ArrayPrototypeIncludes(propagator.fields(), "traceparent")
) {
continue;
}
propagator.inject(context, requestObject.headers, {
set(carrier, key, value) {
carrier.append(key, value);
Expand Down