Skip to content
Merged
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
116 changes: 116 additions & 0 deletions src/instrumentation/libraries/e2e-common/external-http.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
const http = require("http");
const https = require("https");

const EXTERNAL_HTTP_TIMEOUT_MS = Number(process.env.EXTERNAL_HTTP_TIMEOUT_MS || "3000");
const USE_MOCK_EXTERNALS = ["1", "true", "yes"].includes((process.env.USE_MOCK_EXTERNALS || "").toLowerCase());
const MOCK_SERVER_BASE_URL = process.env.MOCK_SERVER_BASE_URL || "http://mock-upstream:8081";

function upstreamUrl(rawUrl) {
if (!USE_MOCK_EXTERNALS) {
return rawUrl;
}
const src = new URL(rawUrl);
const base = new URL(MOCK_SERVER_BASE_URL);
return `${base.origin}${src.pathname}${src.search}`;
}

function withExternalTimeout(init = {}) {
if (init.signal) {
return init;
}

const timeoutSignal = createTimeoutSignal(EXTERNAL_HTTP_TIMEOUT_MS);
return {
...init,
...(timeoutSignal ? { signal: timeoutSignal } : {}),
};
}

function createTimeoutSignal(timeoutMs) {
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
return AbortSignal.timeout(timeoutMs);
}

if (typeof AbortController === "undefined") {
return undefined;
}

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
if (typeof timer.unref === "function") {
timer.unref();
}
controller.signal.addEventListener("abort", () => clearTimeout(timer), { once: true });
return controller.signal;
}

function resolveClient(target) {
return target.protocol === "https:" ? https : http;
}

function getExternalHttpTimeoutMs() {
return EXTERNAL_HTTP_TIMEOUT_MS;
}

function getTextViaNode(rawUrl) {
const target = new URL(upstreamUrl(rawUrl.toString()));
const client = resolveClient(target);
return new Promise((resolve, reject) => {
client
.get(
target,
{
timeout: EXTERNAL_HTTP_TIMEOUT_MS,
},
(response) => {
let data = "";
response.on("data", (chunk) => {
data += chunk;
});
response.on("end", () => resolve(data));
},
)
.on("error", reject);
});
}

function requestTextViaNode(rawUrl, method, body) {
const target = new URL(upstreamUrl(rawUrl.toString()));
const client = resolveClient(target);
return new Promise((resolve, reject) => {
const request = client.request(
{
protocol: target.protocol,
hostname: target.hostname,
port: target.port ? Number(target.port) : target.protocol === "https:" ? 443 : 80,
path: `${target.pathname}${target.search}`,
method,
headers: {
"Content-Type": "application/json",
},
timeout: EXTERNAL_HTTP_TIMEOUT_MS,
},
(response) => {
let data = "";
response.on("data", (chunk) => {
data += chunk;
});
response.on("end", () => resolve(data));
},
);

request.on("error", reject);
if (body) {
request.write(body);
}
request.end();
});
}

module.exports = {
upstreamUrl,
withExternalTimeout,
getExternalHttpTimeoutMs,
getTextViaNode,
requestTextViaNode,
};
115 changes: 115 additions & 0 deletions src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env node

const http = require("http");
const { URL } = require("url");

const port = Number(process.env.MOCK_UPSTREAM_PORT || "8081");

function sendJson(res, payload, status = 200) {
const body = Buffer.from(JSON.stringify(payload));
res.writeHead(status, {
"Content-Type": "application/json",
"Content-Length": String(body.length),
});
res.end(body);
}

function sendText(res, payload, status = 200) {
const body = Buffer.from(payload, "utf-8");
res.writeHead(status, {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": String(body.length),
});
res.end(body);
}

function readBody(req) {
return new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
req.on("error", () => resolve(""));
});
}

function mockPost(id) {
return { id, title: `Mock Post ${id}`, body: `Body for post ${id}`, userId: ((id - 1) % 10) + 1 };
}

const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", `http://localhost:${port}`);
const path = url.pathname;
const method = req.method || "GET";

if (path === "/health") {
return sendJson(res, { status: "ok" });
}

if (method === "GET" && path === "/posts/1") {
return sendJson(res, mockPost(1));
}

if (method === "GET" && path === "/posts") {
const limit = Number(url.searchParams.get("_limit") || "5");
const posts = Array.from({ length: limit }, (_, i) => mockPost(i + 1));
return sendJson(res, posts);
}

if (method === "GET" && path === "/users") {
return sendJson(
res,
Array.from({ length: 10 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
username: `user${i + 1}`,
email: `user${i + 1}@example.com`,
})),
);
}

if (method === "POST" && path === "/posts") {
const raw = await readBody(req);
let parsed = {};
try {
parsed = raw ? JSON.parse(raw) : {};
} catch {
parsed = {};
}
return sendJson(
res,
{
id: 101,
title: parsed.title || "mock-title",
body: parsed.body || "",
userId: parsed.userId || 1,
test: parsed.test || undefined,
},
201,
);
}

if (method === "GET" && path === "/robots.txt") {
return sendText(res, "User-agent: *\nDisallow: /deny\n");
}

if (method === "GET" && url.searchParams.get("format") === "j1") {
const location = decodeURIComponent(path.replace(/^\/+/, "") || "San Francisco");
return sendJson(res, {
current_condition: [
{
temp_F: "72",
humidity: "55",
localObsDateTime: "2026-02-26 07:00 PM",
weatherDesc: [{ value: `Clear (${location})` }],
pressure: "1015",
},
],
});
}

return sendJson(res, { error: `No mock route for ${method} ${path}` }, 404);
});

server.listen(port, "0.0.0.0", () => {
console.log(`Mock upstream listening on :${port}`);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
services:
mock-upstream:
image: node:18
command: ["node", "/mock/mock-server.js"]
working_dir: /mock
volumes:
- ../../../e2e-common/mock-upstream:/mock:ro

app:
build:
context: ../../../../../..
Expand All @@ -12,11 +19,16 @@ services:
- BENCHMARKS=${BENCHMARKS:-}
- BENCHMARK_DURATION=${BENCHMARK_DURATION:-5}
- BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3}
- USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1}
- MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081}
- EXTERNAL_HTTP_TIMEOUT_MS=${EXTERNAL_HTTP_TIMEOUT_MS:-3000}
volumes:
# Mount SDK source for the SDK dependency
- ../../../../../..:/sdk:ro
# Mount app source
- ./src:/app/src
# Mount .tusk config (but traces/logs are created inside container)
- ./.tusk/config.yaml:/app/.tusk/config.yaml:ro
depends_on:
- mock-upstream
working_dir: /app
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { TuskDrift } from "./tdInit";
import express, { Request, Response } from "express";
const { upstreamUrl, withExternalTimeout } = require(
"/sdk/src/instrumentation/libraries/e2e-common/external-http.cjs",
);

const app = express();
const PORT = process.env.PORT || 3000;
Expand All @@ -9,7 +12,10 @@ app.use(express.json());
// Test endpoint using fetch GET
app.get("/test/fetch-get", async (req: Request, res: Response) => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const response = await fetch(
upstreamUrl("https://jsonplaceholder.typicode.com/posts/1"),
withExternalTimeout(),
Comment thread
jy-tan marked this conversation as resolved.
);
const data = await response.json();

res.json({
Expand All @@ -31,13 +37,16 @@ app.get("/test/fetch-get", async (req: Request, res: Response) => {
// Test endpoint using fetch POST
app.post("/test/fetch-post", async (req: Request, res: Response) => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(req.body),
});
const response = await fetch(
upstreamUrl("https://jsonplaceholder.typicode.com/posts"),
withExternalTimeout({
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(req.body),
}),
);

const data = await response.json();

Expand All @@ -58,14 +67,17 @@ app.post("/test/fetch-post", async (req: Request, res: Response) => {
// Test endpoint using fetch with custom headers
app.get("/test/fetch-headers", async (req: Request, res: Response) => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
method: "GET",
headers: {
"User-Agent": "TuskDrift-Test/1.0",
"X-Custom-Header": "test-value",
Accept: "application/json",
},
});
const response = await fetch(
upstreamUrl("https://jsonplaceholder.typicode.com/posts/1"),
withExternalTimeout({
method: "GET",
headers: {
"User-Agent": "TuskDrift-Test/1.0",
"X-Custom-Header": "test-value",
Accept: "application/json",
},
}),
);

const data = await response.json();

Expand All @@ -91,7 +103,10 @@ app.get("/test/fetch-headers", async (req: Request, res: Response) => {
// Test endpoint using fetch with JSON response
app.get("/test/fetch-json", async (req: Request, res: Response) => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const response = await fetch(
upstreamUrl("https://jsonplaceholder.typicode.com/users"),
withExternalTimeout(),
);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
Expand All @@ -116,10 +131,10 @@ app.get("/test/fetch-json", async (req: Request, res: Response) => {
// Test endpoint using fetch with URL object
app.get("/test/fetch-url-object", async (req: Request, res: Response) => {
try {
const url = new URL("https://jsonplaceholder.typicode.com/posts");
const url = new URL(upstreamUrl("https://jsonplaceholder.typicode.com/posts"));
url.searchParams.append("_limit", "5");

const response = await fetch(url);
const response = await fetch(url, withExternalTimeout());
const data = await response.json();

res.json({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
services:
mock-upstream:
image: node:18
command: ["node", "/mock/mock-server.js"]
working_dir: /mock
volumes:
- ../../../e2e-common/mock-upstream:/mock:ro

app:
build:
context: ../../../../../..
Expand All @@ -12,11 +19,16 @@ services:
- BENCHMARKS=${BENCHMARKS:-}
- BENCHMARK_DURATION=${BENCHMARK_DURATION:-5}
- BENCHMARK_WARMUP=${BENCHMARK_WARMUP:-3}
- USE_MOCK_EXTERNALS=${USE_MOCK_EXTERNALS:-1}
- MOCK_SERVER_BASE_URL=${MOCK_SERVER_BASE_URL:-http://mock-upstream:8081}
- EXTERNAL_HTTP_TIMEOUT_MS=${EXTERNAL_HTTP_TIMEOUT_MS:-3000}
volumes:
# Mount SDK source for hot reload (this is what package.json expects)
- ../../../../../..:/sdk:ro
# Mount .tusk config
- ./.tusk/config.yaml:/app/.tusk/config.yaml:ro
# Mount app source for development
- ./src:/app/src
depends_on:
- mock-upstream
working_dir: /app
Loading