-
Notifications
You must be signed in to change notification settings - Fork 165
Expand file tree
/
Copy pathserver.ts
More file actions
153 lines (143 loc) · 6.91 KB
/
Copy pathserver.ts
File metadata and controls
153 lines (143 loc) · 6.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import { SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
import {
ATTR_HTTP_REQUEST_METHOD,
ATTR_HTTP_RESPONSE_STATUS_CODE,
ATTR_URL_FULL,
ATTR_URL_PATH,
ATTR_URL_SCHEME,
} from "@opentelemetry/semantic-conventions";
import * as Sentry from "@sentry/cloudflare";
import handler from "@tanstack/react-start/server-entry";
import { isAppOwnedPath } from "./app-paths";
import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object";
import { browserTracesResponse } from "./observability/browser-traces";
import { flushTracerProvider, installTracerProvider } from "./observability/telemetry";
// ---------------------------------------------------------------------------
// Sentry config
// ---------------------------------------------------------------------------
const sentryOptions = (env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 0,
enableLogs: true,
sendDefaultPii: true,
skipOpenTelemetrySetup: true,
// NOTE: do NOT enable `instrumentPrototypeMethods`. It walks the DO prototype
// and reads every property — including accessors — to find methods to wrap,
// which invokes the `sessionId` getter with `this` bound to the prototype
// (where `ctx` is undefined) and throws during construction, 500ing every
// session create / cold restore. The DO captures its own errors via the
// `captureCause` seam (→ Sentry) instead.
});
// ---------------------------------------------------------------------------
// Durable Object — wrapped with Sentry so DO errors land in Sentry (inits the
// client inside the DO isolate, which plain `Sentry.captureException` cannot
// do on its own). OTEL is installed through Effect layers (observability/telemetry),
// not a global fetch wrapper.
// ---------------------------------------------------------------------------
export const McpSessionDO = Sentry.instrumentDurableObjectWithSentry(
sentryOptions,
McpSessionDOBase,
);
// ---------------------------------------------------------------------------
// Worker fetch handler
//
// We open a single `http.server <METHOD>` span at the worker boundary using
// the same WebTracerProvider that `observability/telemetry.ts` already installs for
// Effect-driven spans. This restores the per-request envelope span that was
// previously emitted by `@microlabs/otel-cf-workers` and lost in the alchemy
// migration — without the OTel-SDK version-conflict that package would now
// drag in (it pins `@opentelemetry/otlp-* ^0.200.0`, we ship ^0.214.0).
//
// ONLY for paths the Effect app does not own. App-owned paths (/api/*, /mcp,
// /.well-known/* — see app-paths.ts) get their `http.server` span from
// Effect's own HttpMiddleware.tracer, which parses `traceparent` itself and
// parents the workos/store/db child spans. Wrapping those here too produced
// two identical sibling `http.server` spans per request (scope
// `executor-cloud-worker` next to scope `executor-cloud`) — double ingest,
// and the waterfall showed a childless twin. The worker span remains for
// everything Effect never sees: Start SSR, the marketing proxy, /_astro
// assets.
//
// SimpleSpanProcessor exports synchronously at span end but the underlying
// `fetch()` to Axiom is fire-and-forget; the Worker may terminate before it
// completes. `ctx.waitUntil(flushTracerProvider())` keeps the isolate alive
// until the in-flight export resolves.
// ---------------------------------------------------------------------------
const fetchHandler = handler.fetch as (
request: Request,
env: Env,
ctx: ExecutionContext,
) => Response | Promise<Response>;
const tracer = trace.getTracer("executor-cloud-worker");
const cloudflareHandler: ExportedHandler<Env> = {
fetch: async (request, env, ctx) => {
// Browser OTLP ingress — before the server span opens: exporter traffic
// must never trace itself (the browser already excludes /v1/traces from
// its own tracing for the same reason).
const browserTraces = browserTracesResponse(request, env);
if (browserTraces) return browserTraces;
if (!installTracerProvider()) {
return fetchHandler(request, env, ctx);
}
const url = new URL(request.url);
// Effect-served paths bring their own http.server span (with traceparent
// join) — opening one here too would duplicate it. See the header note.
if (isAppOwnedPath(url.pathname)) {
// The provider is installed (above) and the flush still must outlive
// the request — Effect's BatchSpanProcessor ships on a timer.
// oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; mirror the traced path's finally
try {
return await fetchHandler(request, env, ctx);
} finally {
ctx.waitUntil(flushTracerProvider());
}
}
// Join the caller's W3C trace when the request carries one — the web UI
// sends traceparent on every API fetch, so the browser's spans and this
// request share one trace id end to end. Same parsing the DO path does
// in session-durable-object.ts.
const traceparentMatch = /^[0-9a-f]{2}-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec(
request.headers.get("traceparent") ?? "",
);
const parentContext = traceparentMatch
? trace.setSpanContext(context.active(), {
traceId: traceparentMatch[1]!,
spanId: traceparentMatch[2]!,
traceFlags: parseInt(traceparentMatch[3]!, 16),
isRemote: true,
})
: context.active();
return tracer.startActiveSpan(
`http.server ${request.method}`,
{ kind: SpanKind.SERVER },
parentContext,
async (span) => {
span.setAttribute(ATTR_HTTP_REQUEST_METHOD, request.method);
span.setAttribute(ATTR_URL_FULL, request.url);
span.setAttribute(ATTR_URL_PATH, url.pathname);
span.setAttribute(ATTR_URL_SCHEME, url.protocol.replace(/:$/, ""));
// Adapter boundary: Cloudflare's fetch handler is a Promise-based
// callback and the OTel span lifecycle needs to observe both the
// resolved response and any thrown error before `span.end()`. Sentry's
// outer wrapper still captures the exception; we only mark span status.
// oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary
try {
const response = await fetchHandler(request, env, ctx);
span.setAttribute(ATTR_HTTP_RESPONSE_STATUS_CODE, response.status);
if (response.status >= 500) {
span.setStatus({ code: SpanStatusCode.ERROR });
}
return response;
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR });
// oxlint-disable-next-line executor/no-try-catch-or-throw -- adapter boundary; preserve original error to Cloudflare runtime
throw err;
} finally {
span.end();
ctx.waitUntil(flushTracerProvider());
}
},
);
},
};
export default Sentry.withSentry(sentryOptions, cloudflareHandler);