Skip to content

Commit 32ba2a9

Browse files
committed
fix(sdks): stop excludeHttpRequests preset from dropping user spans (langwatch#7413)
The HTTP-verb span filter matched case-insensitively with a word boundary, so any user span whose name starts with a verb word followed by punctuation was silently discarded before export: post-process, get-user-profile, delete-account, patch-config, ... while post_process survived, because underscore is a word character. The preset only exists to drop the SDK exporter's own feedback-loop spans, and OpenTelemetry HTTP instrumentation names those with an uppercase verb plus an optional route ('GET', 'POST /v1/traces'). Both the TypeScript predicate and the Go regex now require exactly that shape: case-sensitive verb followed by a space or end of string. Rewrote the pinned case-insensitivity assertions into regressions that keep lowercase user spans alive, added bare-verb coverage, and kept the existing uppercase route-shape cases passing unchanged. Fixes langwatch#7413
1 parent b1bc36f commit 32ba2a9

121 files changed

Lines changed: 16774 additions & 13620 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/llms-full.txt

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

docs/skills/directory.mdx

Lines changed: 237 additions & 5301 deletions
Large diffs are not rendered by default.

docs/skills/pms-and-domain-experts.mdx

Lines changed: 1 addition & 1112 deletions
Large diffs are not rendered by default.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
featureEnv,
4+
LANGY_ENV_KEY,
5+
LEGACY_EVALUATORS_ENV_KEY,
6+
LINGUA_ENV_KEY,
7+
PRESIDIO_ENV_KEY,
8+
resolveFeatures,
9+
} from "../src/shared/features.ts";
10+
11+
describe("optional install pieces", () => {
12+
describe("when nothing is set", () => {
13+
it("installs the assistant and skips every heavyweight evaluator", () => {
14+
const f = resolveFeatures({});
15+
expect(f.isLangyEnabled).toBe(true);
16+
expect(f.isPresidioEnabled).toBe(false);
17+
expect(f.isLinguaEnabled).toBe(false);
18+
expect(f.isLegacyEvaluatorsEnabled).toBe(false);
19+
});
20+
});
21+
22+
describe("when a toggle is set", () => {
23+
it("honours an explicit opt-in to the PII model", () => {
24+
expect(resolveFeatures({ [PRESIDIO_ENV_KEY]: "true" }).isPresidioEnabled).toBe(true);
25+
});
26+
27+
it("honours an explicit opt-out of the assistant", () => {
28+
expect(resolveFeatures({ [LANGY_ENV_KEY]: "false" }).isLangyEnabled).toBe(false);
29+
});
30+
31+
it("honours opting into language detection and legacy evaluators", () => {
32+
expect(resolveFeatures({ [LINGUA_ENV_KEY]: "true" }).isLinguaEnabled).toBe(true);
33+
expect(
34+
resolveFeatures({ [LEGACY_EVALUATORS_ENV_KEY]: "true" }).isLegacyEvaluatorsEnabled,
35+
).toBe(true);
36+
});
37+
38+
it("accepts the spellings people actually type", () => {
39+
for (const yes of ["1", "true", "TRUE", "yes", "on", " true "]) {
40+
expect(resolveFeatures({ [PRESIDIO_ENV_KEY]: yes }).isPresidioEnabled).toBe(true);
41+
}
42+
for (const no of ["0", "false", "FALSE", "no", "off"]) {
43+
expect(resolveFeatures({ [LANGY_ENV_KEY]: no }).isLangyEnabled).toBe(false);
44+
}
45+
});
46+
});
47+
48+
describe("when a toggle is set to something unrecognised", () => {
49+
it("keeps the default rather than reading it as off", () => {
50+
// A typo silently stripping a feature someone asked for is the worse
51+
// failure: they wait for an install that quietly did not happen.
52+
expect(resolveFeatures({ [LANGY_ENV_KEY]: "maybe" }).isLangyEnabled).toBe(true);
53+
expect(resolveFeatures({ [PRESIDIO_ENV_KEY]: "sure" }).isPresidioEnabled).toBe(false);
54+
});
55+
56+
it("treats an empty value as unset", () => {
57+
expect(resolveFeatures({ [LANGY_ENV_KEY]: "" }).isLangyEnabled).toBe(true);
58+
});
59+
});
60+
61+
describe("featureEnv", () => {
62+
it("spells every toggle out so a child process never falls back to a default", () => {
63+
const env = featureEnv(resolveFeatures({ [PRESIDIO_ENV_KEY]: "true" }));
64+
expect(env).toEqual({
65+
[LANGY_ENV_KEY]: "true",
66+
[PRESIDIO_ENV_KEY]: "true",
67+
[LINGUA_ENV_KEY]: "false",
68+
[LEGACY_EVALUATORS_ENV_KEY]: "false",
69+
});
70+
});
71+
});
72+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2+
import { mkdtemp } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, describe, expect, it } from "vitest";
6+
import { restoreShellScriptBits } from "../../src/services/app-dir.ts";
7+
8+
describe("shell script bits after relocation", () => {
9+
const roots: string[] = [];
10+
afterEach(() => {
11+
for (const root of roots.splice(0))
12+
rmSync(root, { recursive: true, force: true });
13+
});
14+
15+
async function makeTree(): Promise<string> {
16+
const root = await mkdtemp(join(tmpdir(), "langwatch-appdir-"));
17+
roots.push(root);
18+
mkdirSync(join(root, "langwatch", "scripts"), { recursive: true });
19+
mkdirSync(join(root, "node_modules", "dep"), { recursive: true });
20+
// pnpm pack normalizes modes: scripts arrive 0644.
21+
writeFileSync(
22+
join(root, "langwatch", "scripts", "build-mcp-server.sh"),
23+
"#!/bin/sh\n",
24+
{
25+
mode: 0o644,
26+
},
27+
);
28+
writeFileSync(
29+
join(root, "langwatch", "scripts", "helper.ts"),
30+
"export {};\n",
31+
{
32+
mode: 0o644,
33+
},
34+
);
35+
writeFileSync(join(root, "node_modules", "dep", "hook.sh"), "#!/bin/sh\n", {
36+
mode: 0o644,
37+
});
38+
return root;
39+
}
40+
41+
describe("when the extracted artifact carries scripts without their bit", () => {
42+
it("makes every *.sh executable so the app's build chain can invoke them", async () => {
43+
// `pnpm run build` calls ./scripts/build-mcp-server.sh directly; a
44+
// 0644 script dies with exit 126 and the boot never reaches healthy.
45+
const root = await makeTree();
46+
const restored = restoreShellScriptBits(root);
47+
expect(restored).toBe(1);
48+
const mode = statSync(
49+
join(root, "langwatch", "scripts", "build-mcp-server.sh"),
50+
).mode;
51+
expect(mode & 0o111).not.toBe(0);
52+
});
53+
54+
it("leaves non-scripts and node_modules alone", async () => {
55+
const root = await makeTree();
56+
restoreShellScriptBits(root);
57+
expect(
58+
statSync(join(root, "langwatch", "scripts", "helper.ts")).mode & 0o111,
59+
).toBe(0);
60+
expect(
61+
statSync(join(root, "node_modules", "dep", "hook.sh")).mode & 0o111,
62+
).toBe(0);
63+
});
64+
});
65+
});
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/env bats
2+
# Unit tests for scripts/dogfood/langy-local.sh, the Langy local dogfood
3+
# doctor. The script under test runs for real against a sandboxed repo
4+
# layout: a temp langwatch/.env, fake opencode/go shims on PATH, and real
5+
# loopback listeners standing in for the app / gateway / langyagent.
6+
#
7+
# Spec: specs/setup/langy-local-dogfood.feature
8+
9+
REPO_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/../.." && pwd)"
10+
11+
setup() {
12+
TEST_DIR="$(mktemp -d)"
13+
mkdir -p "$TEST_DIR/scripts/dogfood" "$TEST_DIR/langwatch" "$TEST_DIR/bin"
14+
cp "$REPO_DIR/scripts/dogfood/langy-local.sh" "$TEST_DIR/scripts/dogfood/"
15+
DOCTOR="$TEST_DIR/scripts/dogfood/langy-local.sh"
16+
ENV_FILE="$TEST_DIR/langwatch/.env"
17+
: >"$ENV_FILE"
18+
19+
# Binaries the doctor requires: fake shims are enough — it only checks PATH.
20+
printf '#!/bin/sh\nexit 0\n' >"$TEST_DIR/bin/opencode"
21+
printf '#!/bin/sh\nexit 0\n' >"$TEST_DIR/bin/go"
22+
chmod +x "$TEST_DIR/bin/opencode" "$TEST_DIR/bin/go"
23+
24+
# A base port slot unlikely to collide; the doctor derives gateway = base+3.
25+
BASE_PORT=$((20000 + (RANDOM % 20000)))
26+
AGENT_PORT=$((BASE_PORT + 7))
27+
LISTENER_PIDS=()
28+
}
29+
30+
teardown() {
31+
for pid in "${LISTENER_PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
32+
rm -rf "$TEST_DIR"
33+
}
34+
35+
listen_on() {
36+
python3 -m http.server "$1" --bind 127.0.0.1 >/dev/null 2>&1 &
37+
LISTENER_PIDS+=("$!")
38+
for _ in $(seq 1 50); do
39+
if lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1; then return 0; fi
40+
sleep 0.1
41+
done
42+
return 1
43+
}
44+
45+
write_full_env() {
46+
mkdir -p "$TEST_DIR/sessions" "$TEST_DIR/workspace"
47+
cat >"$ENV_FILE" <<EOF
48+
OPENCODE_AGENT_URL="http://localhost:${AGENT_PORT}"
49+
LANGY_INTERNAL_SECRET="test-secret"
50+
LANGY_UNSAFE_DEV_DISABLE_ISOLATION=true
51+
SESSIONS_ROOT="$TEST_DIR/sessions"
52+
LANGY_WORKSPACE_ROOT="$TEST_DIR/workspace"
53+
FEATURE_FLAG_FORCE_ENABLE=release_langy_enabled
54+
EOF
55+
}
56+
57+
run_doctor() {
58+
PATH="$TEST_DIR/bin:$PATH" PORT="$BASE_PORT" LANGY_AGENT_PORT="$AGENT_PORT" run "$DOCTOR" "$@"
59+
}
60+
61+
# @scenario "A fully wired setup passes every check"
62+
@test "fully wired setup passes every check and exits zero" {
63+
write_full_env
64+
listen_on "$BASE_PORT"
65+
listen_on $((BASE_PORT + 3))
66+
listen_on "$AGENT_PORT"
67+
68+
run_doctor
69+
[ "$status" -eq 0 ]
70+
[[ "$output" == *"all checks passed"* ]]
71+
[[ "$output" == *"http://localhost:${BASE_PORT}"* ]]
72+
[[ "$output" != *""* ]]
73+
}
74+
75+
# @scenario "A missing env entry prints the exact lines to add"
76+
@test "missing env entries are named with a ready-to-paste block and non-zero exit" {
77+
run_doctor
78+
[ "$status" -ne 0 ]
79+
[[ "$output" == *"LANGY_INTERNAL_SECRET missing"* ]]
80+
[[ "$output" == *"OPENCODE_AGENT_URL missing"* ]]
81+
[[ "$output" == *'LANGY_UNSAFE_DEV_DISABLE_ISOLATION=true'* ]]
82+
[[ "$output" == *"LANGY_INTERNAL_SECRET=\""* ]]
83+
}
84+
85+
# @scenario "A dead service prints the command that starts it"
86+
@test "a dead langyagent names the service and prints its start command" {
87+
write_full_env
88+
listen_on "$BASE_PORT"
89+
listen_on $((BASE_PORT + 3))
90+
91+
run_doctor
92+
[ "$status" -ne 0 ]
93+
[[ "$output" == *"langyagent not listening on :${AGENT_PORT}"* ]]
94+
[[ "$output" == *"service svc=langyagent"* ]]
95+
}
96+
97+
# @scenario "A provider key that the provider rejects is caught before a turn wastes time on it"
98+
@test "a rejected provider key is reported without failing an otherwise green doctor" {
99+
write_full_env
100+
echo 'OPENAI_API_KEY="sk-proj-dead"' >>"$ENV_FILE"
101+
listen_on "$BASE_PORT"
102+
listen_on $((BASE_PORT + 3))
103+
listen_on "$AGENT_PORT"
104+
105+
# A local responder that rejects everything, standing in for the provider.
106+
REJECT_PORT=$((BASE_PORT + 11))
107+
python3 -c '
108+
import http.server, sys
109+
class H(http.server.BaseHTTPRequestHandler):
110+
def do_GET(self):
111+
self.send_response(401); self.end_headers()
112+
def log_message(self, *a): pass
113+
http.server.HTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever()
114+
' "$REJECT_PORT" &
115+
LISTENER_PIDS+=("$!")
116+
for _ in $(seq 1 50); do
117+
lsof -nP -iTCP:"$REJECT_PORT" -sTCP:LISTEN >/dev/null 2>&1 && break
118+
sleep 0.1
119+
done
120+
121+
PATH="$TEST_DIR/bin:$PATH" PORT="$BASE_PORT" LANGY_AGENT_PORT="$AGENT_PORT" \
122+
LANGY_DOCTOR_OPENAI_URL="http://127.0.0.1:${REJECT_PORT}/v1/models" \
123+
run "$DOCTOR"
124+
[ "$status" -eq 0 ]
125+
[[ "$output" == *"OPENAI_API_KEY REJECTED by the provider (HTTP 401)"* ]]
126+
[[ "$output" == *"all checks passed"* ]]
127+
128+
# A userinfo-tricked override (http://localhost:...@attacker) must be
129+
# rejected: the key would otherwise be sent off-machine. curl is stubbed to
130+
# record its argv (no network), so the assertion is that the doctor handed
131+
# curl the REAL fallback endpoint, never the attacker authority.
132+
mkdir -p "$TEST_DIR/curlstub"
133+
cat >"$TEST_DIR/curlstub/curl" <<STUB
134+
#!/bin/sh
135+
printf '%s\n' "\$@" >>"$TEST_DIR/curl-args"
136+
cat >/dev/null
137+
printf '000'
138+
STUB
139+
chmod +x "$TEST_DIR/curlstub/curl"
140+
PATH="$TEST_DIR/curlstub:$TEST_DIR/bin:$PATH" PORT="$BASE_PORT" LANGY_AGENT_PORT="$AGENT_PORT" \
141+
LANGY_DOCTOR_OPENAI_URL="http://localhost:${REJECT_PORT}@attacker.example/v1/models" \
142+
run "$DOCTOR"
143+
[[ "$output" == *"ignoring non-loopback endpoint override"* ]]
144+
grep -q "https://api.openai.com/v1/models" "$TEST_DIR/curl-args"
145+
! grep -q "attacker.example" "$TEST_DIR/curl-args"
146+
}

0 commit comments

Comments
 (0)