Skip to content

Commit 6ceb141

Browse files
bahlojuliusmarminge
authored andcommitted
fix(server): Serialize proxied browser traces with T3CODE_OTLP_PROTOCOL
The browser trace proxy proxied the JSON as-is, so a protobuf-only receiver would reject all browser traces.
1 parent 7115792 commit 6ceb141

2 files changed

Lines changed: 109 additions & 3 deletions

File tree

‎apps/server/src/http.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import * as Schema from "effect/Schema";
1616
import * as Stream from "effect/Stream";
1717
import { cast } from "effect/Function";
1818
import {
19-
HttpBody,
2019
HttpClient,
2120
HttpClientResponse,
2221
HttpMiddleware,
@@ -26,7 +25,7 @@ import {
2625
HttpServerRespondable,
2726
} from "effect/unstable/http";
2827
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
29-
import { OtlpTracer } from "effect/unstable/observability";
28+
import { OtlpTracer, OtlpSerialization } from "effect/unstable/observability";
3029

3130
import * as ServerConfig from "./config.ts";
3231
import { ASSET_ROUTE_PREFIX, resolveAsset } from "./assets/AssetAccess.ts";
@@ -322,6 +321,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
322321
const otlpHeaders = config.otlpHeaders;
323322
const browserTraceCollector = yield* BrowserTraceCollector.BrowserTraceCollector;
324323
const httpClient = yield* HttpClient.HttpClient;
324+
const serialization = yield* OtlpSerialization.OtlpSerialization;
325325
const bodyJson = cast<unknown, OtlpTracer.TraceData>(yield* request.json);
326326

327327
yield* Effect.try({
@@ -343,7 +343,7 @@ export const otlpTracesProxyRouteLayer = HttpRouter.add(
343343

344344
return yield* httpClient
345345
.post(otlpTracesUrl, {
346-
body: HttpBody.jsonUnsafe(bodyJson),
346+
body: serialization.traces(bodyJson),
347347
headers: otlpHeaders,
348348
})
349349
.pipe(

‎apps/server/src/server.test.ts‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ import {
216216
transferBudgetViolations,
217217
} from "../integration/TransferBudgetReport.integration.ts";
218218
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
219+
import { otlpSerializationLayer } from "@t3tools/shared/observability";
219220

220221
const defaultProjectId = ProjectId.make("project-default");
221222
const defaultThreadId = ThreadId.make("thread-default");
@@ -1061,6 +1062,7 @@ const buildAppUnderTest = (options?: {
10611062
...options?.layers?.browserTraceCollector,
10621063
}),
10631064
),
1065+
Layer.provide(otlpSerializationLayer(config.otlpProtocol)),
10641066
Layer.provide(
10651067
Layer.mock(ServerLifecycleEvents.ServerLifecycleEvents)({
10661068
publish: (event) => Effect.succeed({ ...(event as any), sequence: 1 }),
@@ -5217,6 +5219,110 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
52175219
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
52185220
);
52195221

5222+
it.effect("forwards browser OTLP traces as protobuf when the protocol is http/protobuf", () =>
5223+
Effect.gen(function* () {
5224+
const upstreamRequests: Array<{
5225+
readonly body: string;
5226+
readonly contentType: string | null;
5227+
}> = [];
5228+
const localTraceRecords: Array<unknown> = [];
5229+
// Produced by effect's own tracer, so enum fields are numeric and the
5230+
// protobuf encoder accepts them. The hand-written payload in the JSON
5231+
// test uses enum names, which only the JSON path tolerates.
5232+
const payload = yield* makeBrowserOtlpPayload("client.protobuf.test");
5233+
5234+
const collector = yield* Effect.acquireRelease(
5235+
Effect.promise(async () => {
5236+
const NodeHttp = await import("node:http");
5237+
5238+
return await new Promise<{
5239+
readonly close: () => Promise<void>;
5240+
readonly url: string;
5241+
}>((resolve, reject) => {
5242+
const server = NodeHttp.createServer((request, response) => {
5243+
const chunks: Buffer[] = [];
5244+
request.on("data", (chunk) => {
5245+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
5246+
});
5247+
request.on("end", () => {
5248+
upstreamRequests.push({
5249+
body: Buffer.concat(chunks).toString("utf8"),
5250+
contentType: request.headers["content-type"] ?? null,
5251+
});
5252+
response.statusCode = 204;
5253+
response.end();
5254+
});
5255+
});
5256+
5257+
server.on("error", reject);
5258+
server.listen(0, "127.0.0.1", () => {
5259+
const address = server.address();
5260+
if (!address || typeof address === "string") {
5261+
reject(new Error("Expected TCP collector address"));
5262+
return;
5263+
}
5264+
5265+
resolve({
5266+
url: `http://127.0.0.1:${address.port}/v1/traces`,
5267+
close: () =>
5268+
new Promise<void>((resolveClose, rejectClose) => {
5269+
server.close((error) => {
5270+
if (error) {
5271+
rejectClose(error);
5272+
return;
5273+
}
5274+
resolveClose();
5275+
});
5276+
}),
5277+
});
5278+
});
5279+
});
5280+
}),
5281+
({ close }) => Effect.promise(close),
5282+
);
5283+
5284+
yield* buildAppUnderTest({
5285+
config: {
5286+
otlpTracesUrl: collector.url,
5287+
otlpProtocol: "http/protobuf",
5288+
},
5289+
layers: {
5290+
browserTraceCollector: {
5291+
record: (records) =>
5292+
Effect.sync(() => {
5293+
localTraceRecords.push(...records);
5294+
}),
5295+
},
5296+
},
5297+
});
5298+
5299+
const response = yield* HttpClient.post("/api/observability/v1/traces", {
5300+
headers: {
5301+
cookie: yield* getAuthenticatedSessionCookieHeader(),
5302+
"content-type": "application/json",
5303+
},
5304+
// @effect-diagnostics-next-line preferSchemaOverJson:off
5305+
body: HttpBody.text(JSON.stringify(payload), "application/json"),
5306+
});
5307+
5308+
assert.equal(response.status, 204);
5309+
// The local collector still decodes the browser's JSON before forwarding.
5310+
assert.equal(localTraceRecords.length, 1);
5311+
assert.equal(upstreamRequests.length, 1);
5312+
const forwarded = upstreamRequests[0];
5313+
assert.notEqual(forwarded, undefined);
5314+
if (!forwarded) {
5315+
return;
5316+
}
5317+
assert.equal(forwarded.contentType, "application/x-protobuf");
5318+
// Protobuf strings are raw UTF-8, so the span and service names survive
5319+
// the stub's utf8 decode even though the surrounding bytes don't.
5320+
assert.notEqual(forwarded.body[0], "{");
5321+
assert.include(forwarded.body, "client.protobuf.test");
5322+
assert.include(forwarded.body, "t3-web");
5323+
}).pipe(Effect.provide(NodeHttpServer.layerTest)),
5324+
);
5325+
52205326
it.effect("responds to browser OTLP trace preflight requests with CORS headers", () =>
52215327
Effect.gen(function* () {
52225328
yield* buildAppUnderTest();

0 commit comments

Comments
 (0)