History

Scrub span events and cover the browser tracer

Rhys Sullivan committed 3 months ago

Commit 1c65fc81 parent df1187d19 files · +1135 −211Browse files

Showing the changes to packages/plugins/openapi/src (2 of 19 files). Show all

Unified · Split

2 files
  1. packages/plugins/openapi/src/sdk/invoke-telemetry.test.ts +87 −0
  2. packages/plugins/openapi/src/sdk/invoke.ts +5 −2
packages/plugins/openapi/src/sdk/invoke-telemetry.test.ts+87−0
@@ -0,0 +1,87 @@
1+// ---------------------------------------------------------------------------
2+// The invoke span must not carry the connection's base URL verbatim.
3+//
4+// A connection `baseUrl` is a user-supplied URL from the same class as the mcp
5+// and graphql endpoints already covered: `?token=…` in the query string and
6+// `user:pass@host` userinfo are both supported input shapes. Stamping it raw on
7+// `plugin.openapi.invoke` shipped the credential to the trace backend on every
8+// tool call. Synthetic placeholders only.
9+// ---------------------------------------------------------------------------
10+
11+import { describe, expect, it } from "@effect/vitest";
12+import { Effect, Option, Tracer } from "effect";
13+import { FetchHttpClient } from "effect/unstable/http";
14+
15+import { invokeWithLayer } from "./invoke";
16+import { OperationBinding } from "./types";
17+
18+const QUERY_TOKEN = "synthetic-endpoint-token";
19+const USERINFO_PASSWORD = "synthetic-endpoint-password";
20+
21+/** Records every span the program opens so the stamped attributes can be read
22+ * back. Port 1 connection-refuses immediately, so the invocation resolves
23+ * without any network dependency. */
24+const recordingTracer = (spans: Array<Tracer.NativeSpan>) =>
25+ Tracer.make({
26+ span: (options) => {
27+ const span = new Tracer.NativeSpan(options);
28+ spans.push(span);
29+ return span;
30+ },
31+ context: (primitive, fiber) => primitive["~effect/Effect/evaluate"](fiber),
32+ });
33+
34+const operation = OperationBinding.make({
35+ method: "get",
36+ servers: [],
37+ pathTemplate: "/things",
38+ requestBody: Option.none(),
39+ responseBody: Option.none(),
40+ parameters: [],
41+});
42+
43+const invokeAgainst = (baseUrl: string) =>
44+ Effect.gen(function* () {
45+ const spans: Array<Tracer.NativeSpan> = [];
46+ yield* invokeWithLayer(operation, {}, baseUrl, {}, {}, FetchHttpClient.layer).pipe(
47+ Effect.exit,
48+ Effect.provideService(Tracer.Tracer, recordingTracer(spans)),
49+ );
50+ return spans;
51+ });
52+
53+describe("openapi invoke telemetry", () => {
54+ it.effect("stamps a sanitized base_url on the invoke span", () =>
55+ Effect.gen(function* () {
56+ const spans = yield* invokeAgainst(
57+ `http://svc-user:${USERINFO_PASSWORD}@127.0.0.1:1/v1?token=${QUERY_TOKEN}`,
58+ );
59+
60+ const invoke = spans.find((span) => span.name === "plugin.openapi.invoke");
61+ expect(invoke).toBeDefined();
62+ expect(invoke?.attributes.get("plugin.openapi.base_url")).toBe("http://127.0.0.1:1/v1");
63+
64+ // Scoped to the plugin's own spans. Effect's HttpClient separately stamps
65+ // `url.full`/`url.query` on its outgoing client spans; those are scrubbed
66+ // downstream by the export pipeline's redaction, which is not installed
67+ // at this level.
68+ const serialized = JSON.stringify(
69+ spans
70+ .filter((span) => span.name.startsWith("plugin.openapi."))
71+ .map((span) => Object.fromEntries(span.attributes.entries())),
72+ );
73+ expect(serialized).not.toContain(QUERY_TOKEN);
74+ expect(serialized).not.toContain(USERINFO_PASSWORD);
75+ }),
76+ );
77+
78+ it.effect("leaves a credential-free base URL intact", () =>
79+ Effect.gen(function* () {
80+ const spans = yield* invokeAgainst("http://127.0.0.1:1/v1");
81+
82+ const invoke = spans.find((span) => span.name === "plugin.openapi.invoke");
83+ expect(invoke?.attributes.get("plugin.openapi.base_url")).toBe("http://127.0.0.1:1/v1");
84+ expect(invoke?.attributes.get("plugin.openapi.path_template")).toBe("/things");
85+ }),
86+ );
87+});
packages/plugins/openapi/src/sdk/invoke.ts+5−2
@@ -1,6 +1,6 @@
11 import { Effect, Exit, Fiber, Layer, Option, Schema, Stream } from "effect";
22 import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
3−import type { ToolFileValue } from "@executor-js/sdk/core";
3+import { endpointForTelemetry, type ToolFileValue } from "@executor-js/sdk/core";
44
55 import { OpenApiInvocationError } from "./errors";
66 import { isNdjsonMediaType, NDJSON_MEDIA_TYPES, resolveServerUrl } from "./openapi-utils";
@@ -1293,7 +1293,10 @@ export const invokeWithLayer = ( · 1286 unmodified lines
12931293 attributes: {
12941294 "plugin.openapi.method": operation.method.toUpperCase(),
12951295 "plugin.openapi.path_template": operation.pathTemplate,
1296− "plugin.openapi.base_url": effectiveBaseUrl,
1296+ // The connection's `baseUrl` is a user-supplied URL from the same class
1297+ // as the mcp/graphql endpoints: it may carry `?token=…` or `user:pass@`
1298+ // userinfo. Sanitize before stamping.
1299+ "plugin.openapi.base_url": endpointForTelemetry(effectiveBaseUrl),
12971300 },
12981301 }),
12991302 );

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t