Skip to content

Commit 26d11ca

Browse files
JPeer264claude
andcommitted
test(e2e): Port cloudflare-mcp to span streaming
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2d1c8ef commit 26d11ca

2 files changed

Lines changed: 64 additions & 83 deletions

File tree

dev-packages/e2e-tests/test-applications/cloudflare-mcp/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ const mcpHandler = createMcpHandler(createServer, {
6363

6464
export default Sentry.withSentry(
6565
(env: Env) => ({
66-
traceLifecycle: 'static',
6766
dsn: env.E2E_TEST_DSN,
6867
environment: 'qa', // dynamic sampling bias to keep transactions
6968
tunnel: `http://localhost:3031/`, // proxy server
Lines changed: 64 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,17 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForRequest } from '@sentry-internal/test-utils';
2+
import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils';
33

44
const APP_NAME = 'cloudflare-mcp';
55

6-
function getTransaction(eventData: Awaited<ReturnType<typeof waitForRequest>>) {
7-
const event = eventData.envelope[1][0][1];
8-
return typeof event !== 'string' && 'transaction' in event ? event : undefined;
9-
}
10-
11-
function requireTransaction(eventData: Awaited<ReturnType<typeof waitForRequest>>) {
12-
const event = getTransaction(eventData);
13-
if (!event) {
14-
throw new Error('Expected a transaction event');
15-
}
16-
return event;
17-
}
18-
196
test.describe.configure({ mode: 'serial' });
207

218
test('sends spans for MCP 2026-07-28 tool calls', async ({ baseURL }) => {
229
const url = `${baseURL}/mcp?protocol=modern`;
23-
const requestWaiter = waitForRequest(APP_NAME, eventData => {
24-
const event = getTransaction(eventData);
25-
return event?.transaction === 'POST /mcp' && event.contexts?.trace?.data?.['url.full'] === url;
10+
const requestSpanPromise = waitForStreamedSpan(APP_NAME, span => {
11+
return getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.full']?.value === url;
2612
});
27-
const mcpWaiter = waitForRequest(APP_NAME, eventData => {
28-
const event = getTransaction(eventData);
29-
return (
30-
event?.transaction === 'tools/call my-tool' &&
31-
event.contexts?.trace?.data?.['mcp.protocol.version'] === '2026-07-28'
32-
);
13+
const mcpSpanPromise = waitForStreamedSpan(APP_NAME, span => {
14+
return span.name === 'tools/call my-tool' && span.attributes['mcp.protocol.version']?.value === '2026-07-28';
3315
});
3416

3517
const response = await fetch(url, {
@@ -72,61 +54,60 @@ test('sends spans for MCP 2026-07-28 tool calls', async ({ baseURL }) => {
7254
},
7355
});
7456

75-
const requestData = await requestWaiter;
76-
const mcpData = await mcpWaiter;
77-
const requestEvent = requireTransaction(requestData);
78-
const mcpEvent = requireTransaction(mcpData);
79-
const requestTrace = requestEvent.contexts?.trace;
80-
const mcpTrace = mcpEvent.contexts?.trace;
57+
const requestSpan = await requestSpanPromise;
58+
const mcpSpan = await mcpSpanPromise;
8159

82-
expect(requestTrace?.op).toBe('http.server');
83-
expect(requestTrace?.origin).toBe('auto.http.cloudflare');
84-
expect(requestTrace?.status).toBe('ok');
85-
expect(requestTrace?.data?.['sentry.origin']).toBe('auto.http.cloudflare');
86-
expect(requestTrace?.data?.['sentry.op']).toBe('http.server');
87-
expect(requestTrace?.data?.['sentry.segment.name.source']).toBe('url');
88-
expect(requestTrace?.data?.['http.request.method']).toBe('POST');
89-
expect(requestTrace?.data?.['url.path']).toBe('/mcp');
90-
expect(requestTrace?.data?.['url.full']).toBe(url);
91-
expect(requestTrace?.data?.['url.port']).toBe('38787');
92-
expect(requestTrace?.data?.['url.scheme']).toBe('http:');
93-
expect(requestTrace?.data?.['server.address']).toBe('localhost');
94-
expect(requestTrace?.data?.['http.request.body.size']).toBe(341);
95-
expect(requestTrace?.data?.['user_agent.original']).toBe('node');
96-
expect(requestTrace?.data?.['http.request.header.content_type']).toBe('application/json');
97-
expect(requestTrace?.data?.['network.protocol.name']).toBe('http');
98-
expect(requestTrace?.data?.['network.protocol.version']).toBe('1.1');
99-
expect(requestTrace?.data?.['http.response.status_code']).toBe(200);
100-
expect(requestTrace?.data?.['mcp.server.extra']).toBe(' /|\ ^._.^ /|\ ');
101-
expect(mcpTrace?.trace_id).toBe(requestTrace?.trace_id);
102-
expect(mcpTrace?.trace_id).toBe((mcpData.envelope[0].trace as { trace_id: string }).trace_id);
103-
expect(mcpTrace?.parent_span_id).toBe(requestTrace?.span_id);
104-
expect(requestData.envelope[0].event_id).not.toBe(mcpData.envelope[0].event_id);
105-
expect(mcpTrace?.op).toBe('mcp.server');
106-
expect(mcpTrace?.origin).toBe('auto.function.mcp_server');
107-
expect(mcpTrace?.status).toBe('ok');
108-
expect(mcpTrace?.data?.['mcp.transport']).toBe('PerRequestHTTPServerTransport');
109-
expect(mcpTrace?.data?.['network.transport']).toBe('tcp');
110-
expect(mcpTrace?.data?.['mcp.protocol.version']).toBe('2026-07-28');
111-
expect(mcpTrace?.data?.['mcp.client.name']).toBe('cloudflare-modern-client');
112-
expect(mcpTrace?.data?.['mcp.client.version']).toBe('2.0.0');
113-
expect(mcpTrace?.data?.['mcp.server.name']).toBe('cloudflare-mcp');
114-
expect(mcpTrace?.data?.['mcp.server.version']).toBe('2.0.0');
115-
expect(mcpTrace?.data?.['mcp.method.name']).toBe('tools/call');
116-
expect(mcpTrace?.data?.['mcp.request.id']).toBe('modern-tool-call');
117-
expect(mcpTrace?.data?.['mcp.tool.name']).toBe('my-tool');
118-
expect(mcpTrace?.data?.['mcp.request.argument.message']).toBe('"ʕっ•ᴥ•ʔっ"');
119-
expect(mcpTrace?.data?.['mcp.tool.result.content_count']).toBe(1);
120-
expect(mcpTrace?.data?.['mcp.tool.result.content']).toBe('Tool my-tool: ʕっ•ᴥ•ʔっ');
60+
// With span streaming, URL-sourced `http.server` spans are named by method only.
61+
expect(requestSpan.name).toBe('POST');
62+
expect(requestSpan.status).toBe('ok');
63+
expect(requestSpan.attributes).toMatchObject({
64+
'sentry.origin': { value: 'auto.http.cloudflare', type: 'string' },
65+
'sentry.op': { value: 'http.server', type: 'string' },
66+
'sentry.segment.name.source': { value: 'url', type: 'string' },
67+
'http.request.method': { value: 'POST', type: 'string' },
68+
'url.path': { value: '/mcp', type: 'string' },
69+
'url.full': { value: url, type: 'string' },
70+
'url.port': { value: '38787', type: 'string' },
71+
'url.scheme': { value: 'http:', type: 'string' },
72+
'server.address': { value: 'localhost', type: 'string' },
73+
'http.request.body.size': { value: 341, type: 'integer' },
74+
'user_agent.original': { value: 'node', type: 'string' },
75+
'http.request.header.content_type': { value: 'application/json', type: 'string' },
76+
'network.protocol.name': { value: 'http', type: 'string' },
77+
'network.protocol.version': { value: '1.1', type: 'string' },
78+
'http.response.status_code': { value: 200, type: 'integer' },
79+
'mcp.server.extra': { value: ' /|\ ^._.^ /|\ ', type: 'string' },
80+
});
81+
82+
expect(mcpSpan.trace_id).toBe(requestSpan.trace_id);
83+
expect(mcpSpan.parent_span_id).toBe(requestSpan.span_id);
84+
expect(mcpSpan.span_id).not.toBe(requestSpan.span_id);
85+
expect(mcpSpan.status).toBe('ok');
86+
expect(mcpSpan.attributes).toMatchObject({
87+
'sentry.op': { value: 'mcp.server', type: 'string' },
88+
'sentry.origin': { value: 'auto.function.mcp_server', type: 'string' },
89+
'mcp.transport': { value: 'PerRequestHTTPServerTransport', type: 'string' },
90+
'network.transport': { value: 'tcp', type: 'string' },
91+
'mcp.protocol.version': { value: '2026-07-28', type: 'string' },
92+
'mcp.client.name': { value: 'cloudflare-modern-client', type: 'string' },
93+
'mcp.client.version': { value: '2.0.0', type: 'string' },
94+
'mcp.server.name': { value: 'cloudflare-mcp', type: 'string' },
95+
'mcp.server.version': { value: '2.0.0', type: 'string' },
96+
'mcp.method.name': { value: 'tools/call', type: 'string' },
97+
'mcp.request.id': { value: 'modern-tool-call', type: 'string' },
98+
'mcp.tool.name': { value: 'my-tool', type: 'string' },
99+
'mcp.request.argument.message': { value: '"ʕっ•ᴥ•ʔっ"', type: 'string' },
100+
'mcp.tool.result.content_count': { value: 1, type: 'integer' },
101+
'mcp.tool.result.content': { value: 'Tool my-tool: ʕっ•ᴥ•ʔっ', type: 'string' },
102+
});
121103
});
122104

123105
test('keeps sending spans for legacy-compatible MCP tool calls', async ({ baseURL }) => {
124106
const url = `${baseURL}/mcp?protocol=legacy`;
125-
const mcpWaiter = waitForRequest(APP_NAME, eventData => {
126-
const event = getTransaction(eventData);
107+
const mcpSpanPromise = waitForStreamedSpan(APP_NAME, span => {
127108
return (
128-
event?.transaction === 'tools/call my-tool' &&
129-
event.contexts?.trace?.data?.['mcp.request.argument.message'] === '"legacy protocol request"'
109+
span.name === 'tools/call my-tool' &&
110+
span.attributes['mcp.request.argument.message']?.value === '"legacy protocol request"'
130111
);
131112
});
132113

@@ -151,15 +132,16 @@ test('keeps sending spans for legacy-compatible MCP tool calls', async ({ baseUR
151132

152133
expect(response.status).toBe(200);
153134

154-
const mcpEvent = requireTransaction(await mcpWaiter);
155-
const trace = mcpEvent.contexts?.trace;
135+
const mcpSpan = await mcpSpanPromise;
156136

157-
expect(trace?.op).toBe('mcp.server');
158-
expect(trace?.status).toBe('ok');
159-
expect(trace?.data?.['mcp.transport']).toBe('WebStandardStreamableHTTPServerTransport');
160-
expect(trace?.data?.['mcp.method.name']).toBe('tools/call');
161-
expect(trace?.data?.['mcp.request.id']).toBe('legacy-tool-call');
162-
expect(trace?.data?.['mcp.tool.name']).toBe('my-tool');
163-
expect(trace?.data?.['mcp.protocol.version']).toBeUndefined();
164-
expect(trace?.data?.['mcp.tool.result.content']).toBe('Tool my-tool: legacy protocol request');
137+
expect(getSpanOp(mcpSpan)).toBe('mcp.server');
138+
expect(mcpSpan.status).toBe('ok');
139+
expect(mcpSpan.attributes).toMatchObject({
140+
'mcp.transport': { value: 'WebStandardStreamableHTTPServerTransport', type: 'string' },
141+
'mcp.method.name': { value: 'tools/call', type: 'string' },
142+
'mcp.request.id': { value: 'legacy-tool-call', type: 'string' },
143+
'mcp.tool.name': { value: 'my-tool', type: 'string' },
144+
'mcp.tool.result.content': { value: 'Tool my-tool: legacy protocol request', type: 'string' },
145+
});
146+
expect(mcpSpan.attributes['mcp.protocol.version']).toBeUndefined();
165147
});

0 commit comments

Comments
 (0)