Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,30 @@ export const TestDurableObject = Sentry.instrumentDurableObjectWithSentry(
TestDurableObjectBase,
);

export default {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: This is needed, as without it we now don't generate rpc traces. So this was fixed as well

async fetch(request: Request, env: Env): Promise<Response> {
const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test');
const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase;
export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
enableRpcTracePropagation: true,
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
const id: DurableObjectId = env.TEST_DURABLE_OBJECT.idFromName('test');
const stub = env.TEST_DURABLE_OBJECT.get(id) as unknown as TestDurableObjectBase;

if (request.url.includes('hello')) {
const greeting = await stub.sayHello('world');
return new Response(greeting);
}
if (request.url.includes('hello')) {
const greeting = await stub.sayHello('world');
return new Response(greeting);
}

// Test endpoint that modifies and reads a private field via RPC
if (request.url.includes('custom-greeting')) {
await stub.setGreeting('Howdy');
const greeting = await stub.sayHello('partner');
return new Response(greeting);
}
// Test endpoint that modifies and reads a private field via RPC
if (request.url.includes('custom-greeting')) {
await stub.setGreeting('Howdy');
const greeting = await stub.sayHello('partner');
return new Response(greeting);
}

return new Response('Usual response');
},
};
return new Response('Usual response');
},
} satisfies ExportedHandler<Env>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>temp-cloudflare-react</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "cloudflare-agent",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"cf-typegen": "wrangler types --include-runtime=false",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test:dev && pnpm test:prod",
"test:prod": "TEST_ENV=production playwright test",
"test:dev": "TEST_ENV=development playwright test"
},
"dependencies": {
"@cloudflare/ai-chat": "^0.7.1",
"@sentry/cloudflare": "^10.53.1",
"agents": "^0.13.1",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@cloudflare/vite-plugin": "^1.37.2",
"@cloudflare/workers-types": "^4.20260520.1",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@types/node": "^24.12.4",
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"globals": "^17.6.0",
"typescript": "~6.0.3",
"vite": "^8.0.14",
"wrangler": "^4.93.0",
"ws": "^8.20.1"
},
"volta": {
"node": "24.15.0",
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

export default getPlaywrightConfig({
testDir: './tests',
port: 4173,
startCommand: 'pnpm preview',
use: {
baseURL: 'http://localhost:4173',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useState } from 'react';
import { useAgent } from 'agents/react';

function App() {
const [greeting, setGreeting] = useState('');
const [connected, setConnected] = useState(false);

const agent = useAgent({
agent: 'my-agent',
name: 'user-123',
onOpen: () => setConnected(true),
onClose: () => setConnected(false),
});

const handleGreet = async () => {
if (!connected) {
setGreeting('Not connected yet...');
return;
}
try {
const result = await agent.call('greet', ['World']);
setGreeting(result as string);
} catch (err) {
setGreeting(`Error: ${err}`);
console.error('Agent call failed:', err);
}
};

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: '1rem',
}}
>
<button onClick={handleGreet}>Call Agent</button>
{greeting && <p>{greeting}</p>}
<p style={{ fontSize: '0.8rem', color: '#666' }}>{connected ? 'Connected' : 'Connecting...'}</p>
</div>
);
}

export default App;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.tsx';

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({ port: 3031, proxyServerName: 'cloudflare-agent' });
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('@callable() methods work correctly with Sentry instrumentDurableObjectWithSentry', async ({ page, baseURL }) => {
const transactionPromise = waitForTransaction('cloudflare-agent', transactionEvent => {
console.log(transactionEvent);
return (
transactionEvent.transaction === 'GET /agents/my-agent/user-123' &&
transactionEvent.contexts?.trace?.parent_span_id !== undefined
);
});

await page.goto(baseURL!);

await expect(page.getByText('Connected')).toBeVisible();
await page.getByRole('button', { name: 'Call Agent' }).click();
await expect(page.getByText('Hello, World!')).toBeVisible();

const transaction = await transactionPromise;

expect(transaction).toEqual({
contexts: {
trace: {
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
data: expect.any(Object),
op: 'http.server',
status: 'ok',
origin: 'auto.http.cloudflare',
},
cloud_resource: { 'cloud.provider': 'cloudflare' },
culture: { timezone: expect.any(String) },
runtime: { name: 'cloudflare' },
},
spans: expect.arrayContaining([
expect.objectContaining({
op: 'db',
description: 'durable_object_storage_get',
}),
]),
start_timestamp: expect.any(Number),
timestamp: expect.any(Number),
transaction: 'GET /agents/my-agent/user-123',
type: 'transaction',
request: {
headers: expect.any(Object),
method: 'GET',
url: expect.stringContaining('/agents/my-agent/user-123'),
query_string: expect.any(String),
},
transaction_info: { source: 'url' },
platform: 'javascript',
event_id: expect.stringMatching(/[a-f0-9]{32}/),
environment: expect.any(String),
release: expect.any(String),
sdk: {
integrations: expect.any(Array),
name: 'sentry.javascript.cloudflare',
version: expect.any(String),
packages: expect.any(Array),
},
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",

/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.worker.json"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,

/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
"types": ["@cloudflare/workers-types", "vite/client"],
"erasableSyntaxOnly": false
},
"include": ["./worker-configuration.d.ts", "worker"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { cloudflare } from '@cloudflare/vite-plugin';
import agents from 'agents/vite';

// https://vite.dev/config/
export default defineConfig({
plugins: [agents(), react(), cloudflare()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: 603190983556d18d2fa32cebc898cb0b)
interface __BaseEnv_Env {
CF_VERSION_METADATA: WorkerVersionMetadata;
E2E_TEST_DSN: string;
MyAgent: DurableObjectNamespace<import('./worker/index').MyAgent>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import('./worker/index');
durableNamespaces: 'MyAgent';
}
interface Env extends __BaseEnv_Env {}
}
interface Env extends __BaseEnv_Env {}
type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, 'E2E_TEST_DSN'>> {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Sentry from '@sentry/cloudflare';
import { routeAgentRequest, Agent, callable } from 'agents';

class MyBaseAgent extends Agent {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}

export const MyAgent = Sentry.instrumentDurableObjectWithSentry(
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
MyBaseAgent,
);

export default Sentry.withSentry(
(env: Env) => ({
dsn: env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`,
tracesSampleRate: 1,
enableRpcTracePropagation: true,
}),
{
async fetch(request: Request, env: Env): Promise<Response> {
const agentResponse = await routeAgentRequest(request, env);

if (agentResponse) {
return agentResponse;
}

return new Response(null, { status: 404 });
},
} satisfies ExportedHandler<Env>,
);
Loading
Loading