From 8470322e9063f553a752ff36b56ba56f9731b86b Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 23:16:36 -0800 Subject: [PATCH 1/4] Commit --- .../libraries/e2e-common/external-http.cjs | 93 ++++++++++++++ .../e2e-common/mock-upstream/mock-server.js | 100 +++++++++++++++ .../e2e-tests/cjs-fetch/docker-compose.yml | 12 ++ .../fetch/e2e-tests/cjs-fetch/src/index.ts | 53 +++++--- .../e2e-tests/esm-fetch/docker-compose.yml | 12 ++ .../fetch/e2e-tests/esm-fetch/src/index.ts | 56 ++++++--- .../e2e-tests/cjs-http/docker-compose.yml | 12 ++ .../http/e2e-tests/cjs-http/src/index.ts | 116 ++++------------- .../e2e-tests/esm-http/docker-compose.yml | 12 ++ .../http/e2e-tests/esm-http/src/index.ts | 119 +++++------------- 10 files changed, 367 insertions(+), 218 deletions(-) create mode 100644 src/instrumentation/libraries/e2e-common/external-http.cjs create mode 100644 src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js diff --git a/src/instrumentation/libraries/e2e-common/external-http.cjs b/src/instrumentation/libraries/e2e-common/external-http.cjs new file mode 100644 index 00000000..2dfd9682 --- /dev/null +++ b/src/instrumentation/libraries/e2e-common/external-http.cjs @@ -0,0 +1,93 @@ +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 = {}) { + return { + ...init, + signal: init.signal ?? AbortSignal.timeout(EXTERNAL_HTTP_TIMEOUT_MS), + }; +} + +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, +}; diff --git a/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js b/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js new file mode 100644 index 00000000..923addc7 --- /dev/null +++ b/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js @@ -0,0 +1,100 @@ +#!/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"); + } + + 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}`); +}); diff --git a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/docker-compose.yml b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/docker-compose.yml index fe55c42f..736e39af 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/docker-compose.yml +++ b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/docker-compose.yml @@ -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: ../../../../../.. @@ -12,6 +19,9 @@ 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 @@ -19,4 +29,6 @@ services: - ./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 diff --git a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/src/index.ts b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/src/index.ts index ab0ce2e0..7d130370 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/src/index.ts +++ b/src/instrumentation/libraries/fetch/e2e-tests/cjs-fetch/src/index.ts @@ -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; @@ -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(), + ); const data = await response.json(); res.json({ @@ -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(); @@ -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(); @@ -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}`); @@ -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({ diff --git a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/docker-compose.yml b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/docker-compose.yml index 0511deda..f48d37b6 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/docker-compose.yml +++ b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/docker-compose.yml @@ -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: ../../../../../.. @@ -12,6 +19,9 @@ 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 @@ -19,4 +29,6 @@ services: - ./.tusk/config.yaml:/app/.tusk/config.yaml:ro # Mount app source for development - ./src:/app/src + depends_on: + - mock-upstream working_dir: /app diff --git a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/src/index.ts b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/src/index.ts index dc91be66..72756bfd 100644 --- a/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/src/index.ts +++ b/src/instrumentation/libraries/fetch/e2e-tests/esm-fetch/src/index.ts @@ -1,5 +1,11 @@ import { TuskDrift } from "./tdInit.js"; import express, { Request, Response } from "express"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { upstreamUrl, withExternalTimeout } = require( + "/sdk/src/instrumentation/libraries/e2e-common/external-http.cjs", +); const app = express(); const PORT = process.env.PORT || 3000; @@ -9,7 +15,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(), + ); const data = await response.json(); res.json({ @@ -31,13 +40,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(); @@ -58,14 +70,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(); @@ -91,7 +106,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}`); @@ -116,10 +134,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({ diff --git a/src/instrumentation/libraries/http/e2e-tests/cjs-http/docker-compose.yml b/src/instrumentation/libraries/http/e2e-tests/cjs-http/docker-compose.yml index e202e9e3..224889c4 100644 --- a/src/instrumentation/libraries/http/e2e-tests/cjs-http/docker-compose.yml +++ b/src/instrumentation/libraries/http/e2e-tests/cjs-http/docker-compose.yml @@ -1,6 +1,13 @@ version: "3.8" 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: ../../../../../.. @@ -14,6 +21,9 @@ 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 @@ -21,4 +31,6 @@ services: - ./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 diff --git a/src/instrumentation/libraries/http/e2e-tests/cjs-http/src/index.ts b/src/instrumentation/libraries/http/e2e-tests/cjs-http/src/index.ts index be1a587c..0f2f4085 100644 --- a/src/instrumentation/libraries/http/e2e-tests/cjs-http/src/index.ts +++ b/src/instrumentation/libraries/http/e2e-tests/cjs-http/src/index.ts @@ -1,7 +1,9 @@ import { TuskDrift } from "./tdInit"; import http from "http"; -import https from "https"; import axios from "axios"; +const { getExternalHttpTimeoutMs, getTextViaNode, requestTextViaNode, upstreamUrl } = require( + "/sdk/src/instrumentation/libraries/e2e-common/external-http.cjs", +); // Create HTTP server with test endpoints const server = http.createServer(async (req, res) => { @@ -11,19 +13,7 @@ const server = http.createServer(async (req, res) => { try { // Test raw http.get if (url === "/test-http-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - https - .get("https://jsonplaceholder.typicode.com/posts/1", (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode("https://jsonplaceholder.typicode.com/posts/1"); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -37,31 +27,11 @@ const server = http.createServer(async (req, res) => { // Test raw http.request if (url === "/test-http-request" && method === "POST") { - const result = await new Promise((resolve, reject) => { - const options = { - hostname: "jsonplaceholder.typicode.com", - port: 443, - path: "/posts", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const request = https.request(options, (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }); - - request.on("error", reject); - request.write(JSON.stringify({ test: "data" })); - request.end(); - }); + const result = await requestTextViaNode( + "https://jsonplaceholder.typicode.com/posts", + "POST", + JSON.stringify({ test: "data" }), + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -75,19 +45,7 @@ const server = http.createServer(async (req, res) => { // Test https.get if (url === "/test-https-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - https - .get("https://jsonplaceholder.typicode.com/posts/1", (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode("https://jsonplaceholder.typicode.com/posts/1"); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -101,7 +59,9 @@ const server = http.createServer(async (req, res) => { // Test axios GET if (url === "/test-axios-get" && method === "GET") { - const response = await axios.get("https://jsonplaceholder.typicode.com/posts/1"); + const response = await axios.get(upstreamUrl("https://jsonplaceholder.typicode.com/posts/1"), { + timeout: getExternalHttpTimeoutMs(), + }); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -115,9 +75,13 @@ const server = http.createServer(async (req, res) => { // Test axios POST if (url === "/test-axios-post" && method === "POST") { - const response = await axios.post("https://jsonplaceholder.typicode.com/posts", { - test: "data from axios", - }); + const response = await axios.post( + upstreamUrl("https://jsonplaceholder.typicode.com/posts"), + { + test: "data from axios", + }, + { timeout: getExternalHttpTimeoutMs() }, + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -137,20 +101,7 @@ const server = http.createServer(async (req, res) => { } if (url === "/test-url-object-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - const urlObj = new URL("https://jsonplaceholder.typicode.com/posts/1"); - https - .get(urlObj, (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode(new URL("https://jsonplaceholder.typicode.com/posts/1")); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -163,26 +114,11 @@ const server = http.createServer(async (req, res) => { } if (url === "/test-url-object-request" && method === "POST") { - const result = await new Promise((resolve, reject) => { - const urlObj = new URL("https://jsonplaceholder.typicode.com/posts"); - const request = https.request( - urlObj, - { method: "POST", headers: { "Content-Type": "application/json" } }, - (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }, - ); - - request.on("error", reject); - request.write(JSON.stringify({ test: "url-object-request" })); - request.end(); - }); + const result = await requestTextViaNode( + new URL("https://jsonplaceholder.typicode.com/posts"), + "POST", + JSON.stringify({ test: "url-object-request" }), + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( diff --git a/src/instrumentation/libraries/http/e2e-tests/esm-http/docker-compose.yml b/src/instrumentation/libraries/http/e2e-tests/esm-http/docker-compose.yml index feaca731..012ecf9a 100644 --- a/src/instrumentation/libraries/http/e2e-tests/esm-http/docker-compose.yml +++ b/src/instrumentation/libraries/http/e2e-tests/esm-http/docker-compose.yml @@ -1,6 +1,13 @@ version: "3.8" 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: ../../../../../.. @@ -14,6 +21,9 @@ 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 @@ -21,4 +31,6 @@ services: - ./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 diff --git a/src/instrumentation/libraries/http/e2e-tests/esm-http/src/index.ts b/src/instrumentation/libraries/http/e2e-tests/esm-http/src/index.ts index 6d6a7075..e3bee7a7 100644 --- a/src/instrumentation/libraries/http/e2e-tests/esm-http/src/index.ts +++ b/src/instrumentation/libraries/http/e2e-tests/esm-http/src/index.ts @@ -1,7 +1,12 @@ import { TuskDrift } from "./tdInit.js"; import http from "http"; -import https from "https"; import axios from "axios"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { getExternalHttpTimeoutMs, getTextViaNode, requestTextViaNode, upstreamUrl } = require( + "/sdk/src/instrumentation/libraries/e2e-common/external-http.cjs", +); // Create HTTP server with test endpoints const server = http.createServer(async (req, res) => { @@ -11,19 +16,7 @@ const server = http.createServer(async (req, res) => { try { // Test raw http.get if (url === "/test-http-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - https - .get("https://jsonplaceholder.typicode.com/posts/1", (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode("https://jsonplaceholder.typicode.com/posts/1"); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -37,31 +30,11 @@ const server = http.createServer(async (req, res) => { // Test raw http.request if (url === "/test-http-request" && method === "POST") { - const result = await new Promise((resolve, reject) => { - const options = { - hostname: "jsonplaceholder.typicode.com", - port: 443, - path: "/posts", - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }; - - const request = https.request(options, (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }); - - request.on("error", reject); - request.write(JSON.stringify({ test: "data" })); - request.end(); - }); + const result = await requestTextViaNode( + "https://jsonplaceholder.typicode.com/posts", + "POST", + JSON.stringify({ test: "data" }), + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -75,19 +48,7 @@ const server = http.createServer(async (req, res) => { // Test https.get if (url === "/test-https-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - https - .get("https://jsonplaceholder.typicode.com/posts/1", (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode("https://jsonplaceholder.typicode.com/posts/1"); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -101,7 +62,9 @@ const server = http.createServer(async (req, res) => { // Test axios GET if (url === "/test-axios-get" && method === "GET") { - const response = await axios.get("https://jsonplaceholder.typicode.com/posts/1"); + const response = await axios.get(upstreamUrl("https://jsonplaceholder.typicode.com/posts/1"), { + timeout: getExternalHttpTimeoutMs(), + }); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -115,9 +78,13 @@ const server = http.createServer(async (req, res) => { // Test axios POST if (url === "/test-axios-post" && method === "POST") { - const response = await axios.post("https://jsonplaceholder.typicode.com/posts", { - test: "data from axios", - }); + const response = await axios.post( + upstreamUrl("https://jsonplaceholder.typicode.com/posts"), + { + test: "data from axios", + }, + { timeout: getExternalHttpTimeoutMs() }, + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -137,20 +104,7 @@ const server = http.createServer(async (req, res) => { } if (url === "/test-url-object-get" && method === "GET") { - const result = await new Promise((resolve, reject) => { - const urlObj = new URL("https://jsonplaceholder.typicode.com/posts/1"); - https - .get(urlObj, (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }) - .on("error", reject); - }); + const result = await getTextViaNode(new URL("https://jsonplaceholder.typicode.com/posts/1")); res.writeHead(200, { "Content-Type": "application/json" }); res.end( @@ -163,26 +117,11 @@ const server = http.createServer(async (req, res) => { } if (url === "/test-url-object-request" && method === "POST") { - const result = await new Promise((resolve, reject) => { - const urlObj = new URL("https://jsonplaceholder.typicode.com/posts"); - const request = https.request( - urlObj, - { method: "POST", headers: { "Content-Type": "application/json" } }, - (response) => { - let data = ""; - response.on("data", (chunk) => { - data += chunk; - }); - response.on("end", () => { - resolve(data); - }); - }, - ); - - request.on("error", reject); - request.write(JSON.stringify({ test: "url-object-request" })); - request.end(); - }); + const result = await requestTextViaNode( + new URL("https://jsonplaceholder.typicode.com/posts"), + "POST", + JSON.stringify({ test: "url-object-request" }), + ); res.writeHead(200, { "Content-Type": "application/json" }); res.end( From d5a0fc49a5efb3610a49015b41a9f6c144af94db Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 23:30:44 -0800 Subject: [PATCH 2/4] Fix --- .../libraries/e2e-common/external-http.cjs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/instrumentation/libraries/e2e-common/external-http.cjs b/src/instrumentation/libraries/e2e-common/external-http.cjs index 2dfd9682..c8b002dc 100644 --- a/src/instrumentation/libraries/e2e-common/external-http.cjs +++ b/src/instrumentation/libraries/e2e-common/external-http.cjs @@ -15,12 +15,35 @@ function upstreamUrl(rawUrl) { } function withExternalTimeout(init = {}) { + if (init.signal) { + return init; + } + + const timeoutSignal = createTimeoutSignal(EXTERNAL_HTTP_TIMEOUT_MS); return { ...init, - signal: init.signal ?? AbortSignal.timeout(EXTERNAL_HTTP_TIMEOUT_MS), + ...(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; } From 3e7dc2f5dc5f4943bf3f332b5cf2e85d28c33a89 Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 23:46:33 -0800 Subject: [PATCH 3/4] Update nextjs --- .../e2e-common/mock-upstream/mock-server.js | 15 ++++ .../e2e-tests/cjs-nextjs/docker-compose.yml | 12 +++ .../cjs-nextjs/src/app/api/weather/route.ts | 83 ++++++++---------- .../e2e-tests/esm-nextjs/docker-compose.yml | 12 +++ .../esm-nextjs/src/app/api/weather/route.ts | 85 ++++++++----------- 5 files changed, 109 insertions(+), 98 deletions(-) diff --git a/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js b/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js index 923addc7..1f6928cc 100644 --- a/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js +++ b/src/instrumentation/libraries/e2e-common/mock-upstream/mock-server.js @@ -92,6 +92,21 @@ const server = http.createServer(async (req, res) => { 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); }); diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/docker-compose.yml b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/docker-compose.yml index 27850756..1d93c349 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/docker-compose.yml +++ b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/docker-compose.yml @@ -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: ../../../../../.. @@ -13,6 +20,9 @@ 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 @@ -24,4 +34,6 @@ services: - ./src:/app/src # Mount Next.js config files - ./next.config.js:/app/next.config.js + depends_on: + - mock-upstream working_dir: /app diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts index 4d27694e..3dc1f780 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts +++ b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts @@ -1,6 +1,33 @@ import { NextRequest, NextResponse } from "next/server"; import axios from "axios"; +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 weatherUrl(location: string): string { + if (USE_MOCK_EXTERNALS) { + return `${MOCK_SERVER_BASE_URL}/${encodeURIComponent(location)}?format=j1`; + } + return `http://wttr.in/${encodeURIComponent(location)}?format=j1`; +} + +function buildWeatherPayload(location: string, data: any, fallback = false) { + const currentCondition = data?.current_condition?.[0] || {}; + return { + location, + current: { + temp_F: currentCondition.temp_F ?? "72", + humidity: currentCondition.humidity ?? "55", + localObsDateTime: currentCondition.localObsDateTime ?? "unknown", + weatherDesc: currentCondition.weatherDesc?.[0]?.value ?? "Clear", + pressure: currentCondition.pressure ?? "1015", + }, + source: USE_MOCK_EXTERNALS ? "mock-upstream" : "wttr.in", + ...(fallback ? { fallback: true } : {}), + }; +} + export async function GET(request: NextRequest) { try { // Get location from query params @@ -10,38 +37,17 @@ export async function GET(request: NextRequest) { // Default to a generic location if none provided const weatherLocation = location || "San Francisco"; - const response = await axios.get( - `http://wttr.in/${encodeURIComponent(weatherLocation)}?format=j1`, - ); - - // Extract only the requested fields from current conditions - const currentCondition = response.data.current_condition[0]; - const current = { - temp_F: currentCondition.temp_F, - humidity: currentCondition.humidity, - localObsDateTime: currentCondition.localObsDateTime, - weatherDesc: currentCondition.weatherDesc[0].value, - pressure: currentCondition.pressure, - }; + const response = await axios.get(weatherUrl(weatherLocation), { timeout: EXTERNAL_HTTP_TIMEOUT_MS }); - return NextResponse.json({ - location: weatherLocation, - current, - source: "wttr.in", - }); + return NextResponse.json(buildWeatherPayload(weatherLocation, response.data)); } catch (error) { console.error("Error getting weather data", { error, location: request.nextUrl.searchParams.get("location"), }); - return NextResponse.json( - { - error: "Failed to fetch weather data", - message: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 }, - ); + const fallbackLocation = request.nextUrl.searchParams.get("location") || "San Francisco"; + return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); } } @@ -56,32 +62,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "Location is required in request body" }, { status: 400 }); } - const response = await axios.get(`http://wttr.in/${encodeURIComponent(location)}?format=j1`); + const response = await axios.get(weatherUrl(location), { timeout: EXTERNAL_HTTP_TIMEOUT_MS }); - // Extract only the requested fields from current conditions - const currentCondition = response.data.current_condition[0]; - const current = { - temp_F: currentCondition.temp_F, - humidity: currentCondition.humidity, - localObsDateTime: currentCondition.localObsDateTime, - weatherDesc: currentCondition.weatherDesc[0].value, - pressure: currentCondition.pressure, - }; - - return NextResponse.json({ - location: location, - current, - source: "wttr.in", - }); + return NextResponse.json(buildWeatherPayload(location, response.data)); } catch (error) { console.error("Error getting weather data (POST)", { error }); - return NextResponse.json( - { - error: "Failed to fetch weather data", - message: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 }, - ); + const fallbackLocation = (await request.clone().json().catch(() => ({}))).location || "San Francisco"; + return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); } } diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/docker-compose.yml b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/docker-compose.yml index aef10c0c..2abec915 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/docker-compose.yml +++ b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/docker-compose.yml @@ -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: ../../../../../.. @@ -13,6 +20,9 @@ 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 @@ -24,4 +34,6 @@ services: - ./src:/app/src # Mount Next.js config files - ./next.config.mjs:/app/next.config.mjs + depends_on: + - mock-upstream working_dir: /app diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts index 0e25e67b..8a63171e 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts +++ b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts @@ -1,6 +1,33 @@ import { NextRequest, NextResponse } from "next/server"; import axios from "axios"; +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 weatherUrl(location: string): string { + if (USE_MOCK_EXTERNALS) { + return `${MOCK_SERVER_BASE_URL}/${encodeURIComponent(location)}?format=j1`; + } + return `http://wttr.in/${encodeURIComponent(location)}?format=j1`; +} + +function buildWeatherPayload(location: string, data: any, fallback = false) { + const currentCondition = data?.current_condition?.[0] || {}; + return { + location, + current: { + temp_F: currentCondition.temp_F ?? "72", + humidity: currentCondition.humidity ?? "55", + localObsDateTime: currentCondition.localObsDateTime ?? "unknown", + weatherDesc: currentCondition.weatherDesc?.[0]?.value ?? "Clear", + pressure: currentCondition.pressure ?? "1015", + }, + source: USE_MOCK_EXTERNALS ? "mock-upstream" : "wttr.in", + ...(fallback ? { fallback: true } : {}), + }; +} + export async function GET(request: NextRequest) { try { // Get location from query params @@ -10,39 +37,18 @@ export async function GET(request: NextRequest) { // Default to a generic location if none provided const weatherLocation = location || "San Francisco"; - const response = await axios.get( - `http://wttr.in/${encodeURIComponent(weatherLocation)}?format=j1`, - ); + const response = await axios.get(weatherUrl(weatherLocation), { timeout: EXTERNAL_HTTP_TIMEOUT_MS }); console.log("Weather API call successful", { location: weatherLocation, }); - // Extract only the requested fields from current conditions - const currentCondition = response.data.current_condition[0]; - const current = { - temp_F: currentCondition.temp_F, - humidity: currentCondition.humidity, - localObsDateTime: currentCondition.localObsDateTime, - weatherDesc: currentCondition.weatherDesc[0].value, - pressure: currentCondition.pressure, - }; - - return NextResponse.json({ - location: weatherLocation, - current, - source: "wttr.in", - }); + return NextResponse.json(buildWeatherPayload(weatherLocation, response.data)); } catch (error) { console.error("Error getting weather data", { error, location: request.nextUrl.searchParams.get("location") }); - return NextResponse.json( - { - error: "Failed to fetch weather data", - message: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 } - ); + const fallbackLocation = request.nextUrl.searchParams.get("location") || "San Francisco"; + return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); } } @@ -60,38 +66,17 @@ export async function POST(request: NextRequest) { ); } - const response = await axios.get( - `http://wttr.in/${encodeURIComponent(location)}?format=j1`, - ); + const response = await axios.get(weatherUrl(location), { timeout: EXTERNAL_HTTP_TIMEOUT_MS }); console.log("Weather API call successful (POST)", { location: location, }); - // Extract only the requested fields from current conditions - const currentCondition = response.data.current_condition[0]; - const current = { - temp_F: currentCondition.temp_F, - humidity: currentCondition.humidity, - localObsDateTime: currentCondition.localObsDateTime, - weatherDesc: currentCondition.weatherDesc[0].value, - pressure: currentCondition.pressure, - }; - - return NextResponse.json({ - location: location, - current, - source: "wttr.in", - }); + return NextResponse.json(buildWeatherPayload(location, response.data)); } catch (error) { console.error("Error getting weather data (POST)", { error }); - return NextResponse.json( - { - error: "Failed to fetch weather data", - message: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 } - ); + const fallbackLocation = (await request.clone().json().catch(() => ({}))).location || "San Francisco"; + return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); } } From 7257bed67d2b09b2a647b61291f9c00af079ef68 Mon Sep 17 00:00:00 2001 From: JY Tan Date: Wed, 25 Feb 2026 23:58:16 -0800 Subject: [PATCH 4/4] Fix nextjs --- .../cjs-nextjs/src/app/api/weather/route.ts | 18 ++++++++++++++++-- .../esm-nextjs/src/app/api/weather/route.ts | 18 ++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts index 3dc1f780..aef6b646 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts +++ b/src/instrumentation/libraries/nextjs/e2e-tests/cjs-nextjs/src/app/api/weather/route.ts @@ -47,7 +47,14 @@ export async function GET(request: NextRequest) { }); const fallbackLocation = request.nextUrl.searchParams.get("location") || "San Francisco"; - return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); + return NextResponse.json( + { + ...buildWeatherPayload(fallbackLocation, {}, true), + error: "Failed to fetch weather data", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 502 }, + ); } } @@ -69,6 +76,13 @@ export async function POST(request: NextRequest) { console.error("Error getting weather data (POST)", { error }); const fallbackLocation = (await request.clone().json().catch(() => ({}))).location || "San Francisco"; - return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); + return NextResponse.json( + { + ...buildWeatherPayload(fallbackLocation, {}, true), + error: "Failed to fetch weather data", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 502 }, + ); } } diff --git a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts index 8a63171e..00b1a57d 100644 --- a/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts +++ b/src/instrumentation/libraries/nextjs/e2e-tests/esm-nextjs/src/app/api/weather/route.ts @@ -48,7 +48,14 @@ export async function GET(request: NextRequest) { console.error("Error getting weather data", { error, location: request.nextUrl.searchParams.get("location") }); const fallbackLocation = request.nextUrl.searchParams.get("location") || "San Francisco"; - return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); + return NextResponse.json( + { + ...buildWeatherPayload(fallbackLocation, {}, true), + error: "Failed to fetch weather data", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 502 }, + ); } } @@ -77,6 +84,13 @@ export async function POST(request: NextRequest) { console.error("Error getting weather data (POST)", { error }); const fallbackLocation = (await request.clone().json().catch(() => ({}))).location || "San Francisco"; - return NextResponse.json(buildWeatherPayload(fallbackLocation, {}, true)); + return NextResponse.json( + { + ...buildWeatherPayload(fallbackLocation, {}, true), + error: "Failed to fetch weather data", + message: error instanceof Error ? error.message : "Unknown error", + }, + { status: 502 }, + ); } }