Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ jobs:
run: yarn lint
- name: Lint for ES compatibility
run: yarn lint:es-compatibility
- name: Type check cloudflare integration test suites
working-directory: dev-packages/cloudflare-integration-tests
run: yarn type-check

job_check_lockfile:
name: Check lockfile
Expand Down
1 change: 1 addition & 0 deletions dev-packages/cloudflare-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"scripts": {
"lint": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --type-aware",
"lint:fix": "OXLINT_TSGOLINT_DANGEROUSLY_SUPPRESS_PROGRAM_DIAGNOSTICS=true oxlint . --fix --type-aware",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:watch": "yarn test --watch"
},
Expand Down
5 changes: 4 additions & 1 deletion dev-packages/cloudflare-integration-tests/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,10 @@ export function createRunner(...paths: string[]) {
expected: Expected | Expected[],
options: { headers?: Record<string, string>; data?: BodyInit; expectError?: boolean } = {},
): Promise<T | undefined> {
const expectations = Array.isArray(expected) ? expected : [expected];
// `Expected` includes `Envelope`, which is itself an array, so `Array.isArray` can't
// distinguish a single `Envelope` from an `Expected[]`. Callers pass expectation
// callbacks (or an array of them), so the narrowed value is always `Expected[]`.
const expectations = (Array.isArray(expected) ? expected : [expected]) as Expected[];
const envelopePromises = expectations.map(e => waitForEnvelope(e));
const result = await this.makeRequest<T>(method, path, options);
await Promise.all(envelopePromises);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Envelope } from '@sentry/core';
import type { Envelope, TransactionEvent } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { expect, it } from 'vitest';
import { createRunner } from '../../../runner';
Expand All @@ -14,7 +14,7 @@ const flushMarkerMatcher = (envelope: Envelope): void => {
it('instruments SQL exec operations on Durable Object storage', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent | undefined;
const spans = transactionEvent?.spans ?? [];

expect(transactionEvent).toEqual(
Expand All @@ -24,9 +24,7 @@ it('instruments SQL exec operations on Durable Object storage', async ({ signal
}),
);

const sqlSpans = (spans as Array<Record<string, unknown>>).filter(
s => s.origin === 'auto.db.cloudflare.durable_object.sql',
);
const sqlSpans = spans.filter(s => s.origin === 'auto.db.cloudflare.durable_object.sql');

expect(sqlSpans).toHaveLength(3);
expect(sqlSpans).toEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Envelope } from '@sentry/core';
import type { Envelope, TransactionEvent } from '@sentry/core';
import { expect, it } from 'vitest';
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { createRunner } from '../../../runner';
Expand All @@ -14,7 +14,7 @@ const flushMarkerMatcher = (envelope: Envelope): void => {
it('instruments sync KV operations on Durable Object storage', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
const transactionEvent = envelope[1]?.[0]?.[1] as TransactionEvent | undefined;
const spans = transactionEvent?.spans ?? [];

expect(transactionEvent).toEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, it } from 'vitest';
import type { SerializedStreamedSpan } from '@sentry/core';
import {
GEN_AI_OPERATION_NAME_ATTRIBUTE,
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
Expand Down Expand Up @@ -29,13 +30,13 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
const container = envelope[1]?.[1]?.[1] as any;
expect(container).toBeDefined();
expect(container.items).toHaveLength(3);
expect(container.items.map(span => span.name).sort()).toEqual([
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
'chat gemini-1.5-pro',
'embeddings text-embedding-004',
'generate_content gemini-1.5-flash',
]);

const chatSpan = container.items.find(span => span.name === 'chat gemini-1.5-pro');
const chatSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'chat gemini-1.5-pro');
expect(chatSpan).toBeDefined();
expect(chatSpan!.status).toBe('ok');
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' });
Expand All @@ -50,7 +51,9 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 12 });
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 20 });

const generateContentSpan = container.items.find(span => span.name === 'generate_content gemini-1.5-flash');
const generateContentSpan = container.items.find(
(span: SerializedStreamedSpan) => span.name === 'generate_content gemini-1.5-flash',
);
expect(generateContentSpan).toBeDefined();
expect(generateContentSpan!.status).toBe('ok');
expect(generateContentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
Expand Down Expand Up @@ -95,7 +98,9 @@ it('traces Google GenAI chat creation and message sending', async ({ signal }) =
value: 20,
});

const embeddingsSpan = container.items.find(span => span.name === 'embeddings text-embedding-004');
const embeddingsSpan = container.items.find(
(span: SerializedStreamedSpan) => span.name === 'embeddings text-embedding-004',
);
expect(embeddingsSpan).toBeDefined();
expect(embeddingsSpan!.status).toBe('ok');
expect(embeddingsSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/cloudflare';
import { MockChain, MockChatModel, MockTool } from './mocks';
import { type CallbackHandler, MockChain, MockChatModel, MockTool } from './mocks';

interface Env {
SENTRY_DSN: string;
Expand All @@ -14,10 +14,11 @@ export default Sentry.withSentry(
{
async fetch(_request, _env, _ctx) {
// Create LangChain callback handler
// The mock models accept their own local `CallbackHandler` shape, which differs from the SDK's handler type.
const callbackHandler = Sentry.createLangChainCallbackHandler({
recordInputs: false,
recordOutputs: false,
});
}) as unknown as CallbackHandler;

// Test 1: Chat model invocation
const chatModel = new MockChatModel({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, it } from 'vitest';
import type { SerializedStreamedSpan } from '@sentry/core';
import {
GEN_AI_OPERATION_NAME_ATTRIBUTE,
GEN_AI_REQUEST_MAX_TOKENS_ATTRIBUTE,
Expand Down Expand Up @@ -29,13 +30,15 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal }
const container = envelope[1]?.[1]?.[1] as any;
expect(container).toBeDefined();
expect(container.items).toHaveLength(3);
expect(container.items.map(span => span.name).sort()).toEqual([
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
'chain my_test_chain',
'chat claude-3-5-sonnet-20241022',
'execute_tool search_tool',
]);

const chatSpan = container.items.find(span => span.name === 'chat claude-3-5-sonnet-20241022');
const chatSpan = container.items.find(
(span: SerializedStreamedSpan) => span.name === 'chat claude-3-5-sonnet-20241022',
);
expect(chatSpan).toBeDefined();
expect(chatSpan!.status).toBe('ok');
expect(chatSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({ type: 'string', value: 'chat' });
Expand All @@ -52,14 +55,14 @@ it('traces langchain chat model, chain, and tool invocations', async ({ signal }
expect(chatSpan!.attributes[GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 15 });
expect(chatSpan!.attributes[GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]).toEqual({ type: 'integer', value: 25 });

const chainSpan = container.items.find(span => span.name === 'chain my_test_chain');
const chainSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'chain my_test_chain');
expect(chainSpan).toBeDefined();
expect(chainSpan!.status).toBe('ok');
expect(chainSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' });
expect(chainSpan!.attributes['sentry.op']).toEqual({ type: 'string', value: 'gen_ai.invoke_agent' });
expect(chainSpan!.attributes['langchain.chain.name']).toEqual({ type: 'string', value: 'my_test_chain' });

const toolSpan = container.items.find(span => span.name === 'execute_tool search_tool');
const toolSpan = container.items.find((span: SerializedStreamedSpan) => span.name === 'execute_tool search_tool');
expect(toolSpan).toBeDefined();
expect(toolSpan!.status).toBe('ok');
expect(toolSpan!.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ai.langchain' });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, it } from 'vitest';
import type { SerializedStreamedSpan } from '@sentry/core';
import {
GEN_AI_AGENT_NAME_ATTRIBUTE,
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
Expand Down Expand Up @@ -29,12 +30,14 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
expect(container).toBeDefined();

expect(container.items).toHaveLength(2);
expect(container.items.map(span => span.name).sort()).toEqual([
expect(container.items.map((span: SerializedStreamedSpan) => span.name).sort()).toEqual([
'create_agent weather_assistant',
'invoke_agent weather_assistant',
]);

const createAgentSpan = container.items.find(span => span.name === 'create_agent weather_assistant');
const createAgentSpan = container.items.find(
(span: SerializedStreamedSpan) => span.name === 'create_agent weather_assistant',
);
expect(createAgentSpan).toBeDefined();
expect(createAgentSpan!.status).toBe('ok');
expect(createAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
Expand All @@ -48,7 +51,9 @@ it('traces langgraph compile and invoke operations', async ({ signal }) => {
value: 'weather_assistant',
});

const invokeAgentSpan = container.items.find(span => span.name === 'invoke_agent weather_assistant');
const invokeAgentSpan = container.items.find(
(span: SerializedStreamedSpan) => span.name === 'invoke_agent weather_assistant',
);
expect(invokeAgentSpan).toBeDefined();
expect(invokeAgentSpan!.status).toBe('ok');
expect(invokeAgentSpan!.attributes[GEN_AI_OPERATION_NAME_ATTRIBUTE]).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export default Sentry.withSentry(
}),
{
async fetch(_request, _env, _ctx) {
const response = await client.chat?.completions?.create({
// The mock client types `chat` loosely (`Record<string, unknown>`), so narrow it to the shape used here.
const completions = (
client.chat as { completions?: { create: (args: Record<string, unknown>) => Promise<unknown> } }
)?.completions;
const response = await completions?.create({
model: 'gpt-3.5-turbo',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';
import type { RpcTarget } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObjectBase>;
}

class MyDurableObjectBase extends DurableObject<Env> implements RpcTarget {
class MyDurableObjectBase extends DurableObject<Env> {
async sayHello(name: string): Promise<string> {
return `Hello, ${name}!`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject } from 'cloudflare:workers';
import type { RpcTarget } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObjectBase>;
}

class MyDurableObjectBase extends DurableObject<Env> implements RpcTarget {
class MyDurableObjectBase extends DurableObject<Env> {
async computeAnswer(): Promise<number> {
return 42;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface Env {
SENTRY_DSN: string;
}

class MySubWorkerEntrypointBase extends WorkerEntrypoint<Env> {
class MySubWorkerEntrypointBase extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
MyDurableObjectBase,
);

class MyWorkerEntrypointBase extends WorkerEntrypoint<Env> {
class MyWorkerEntrypointBase extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const id = this.env.MY_DURABLE_OBJECT.idFromName('test');
const stub = this.env.MY_DURABLE_OBJECT.get(id);
const id = (this.env as Env).MY_DURABLE_OBJECT.idFromName('test');
const stub = (this.env as Env).MY_DURABLE_OBJECT.get(id);

if (url.pathname === '/do/hello') {
const doResponse = await stub.fetch(new Request('http://do/hello'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
class MyWorkerEntrypointBase extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const id = this.env.MY_DURABLE_OBJECT.idFromName('test');
const stub = this.env.MY_DURABLE_OBJECT.get(id);
const id = (this.env as Env).MY_DURABLE_OBJECT.idFromName('test');
const stub = (this.env as Env).MY_DURABLE_OBJECT.get(id);

if (url.pathname === '/rpc/hello') {
const result = await stub.sayHello('World');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as Sentry from '@sentry/cloudflare';
import { DurableObject, WorkerEntrypoint } from 'cloudflare:workers';
import type { RpcTarget } from 'cloudflare:workers';

interface Env {
SENTRY_DSN: string;
MY_DURABLE_OBJECT: DurableObjectNamespace<MyDurableObjectBase>;
}

class MyDurableObjectBase extends DurableObject<Env> implements RpcTarget {
class MyDurableObjectBase extends DurableObject<Env> {
async computeAnswer(): Promise<number> {
return 42;
}
Expand All @@ -22,13 +21,13 @@ export const MyDurableObject = Sentry.instrumentDurableObjectWithSentry(
MyDurableObjectBase,
);

class MySubWorkerEntrypointBase extends WorkerEntrypoint<Env> {
class MySubWorkerEntrypointBase extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);

if (url.pathname === '/call-do') {
const id = this.env.MY_DURABLE_OBJECT.idFromName('test');
const stub = this.env.MY_DURABLE_OBJECT.get(id);
const id = (this.env as Env).MY_DURABLE_OBJECT.idFromName('test');
const stub = (this.env as Env).MY_DURABLE_OBJECT.get(id);
const result = await stub.computeAnswer();
return new Response(`The answer is ${result}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ interface Env {
SUB_WORKER: Fetcher;
}

class MyWorkerEntrypointBase extends WorkerEntrypoint<Env> {
class MyWorkerEntrypointBase extends WorkerEntrypoint {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);

if (url.pathname === '/chain') {
const response = await this.env.SUB_WORKER.fetch(new Request('http://fake-host/call-do'));
const response = await (this.env as Env).SUB_WORKER.fetch(new Request('http://fake-host/call-do'));
const text = await response.text();
return new Response(text);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default Sentry.withSentry(
content: [{ type: 'text', text: 'Hello from mock AI!' }],
warnings: [],
}),
}),
// The mock result shape differs from this `ai` version's LanguageModelV3 result type; cast to the mock's expected config.
} as unknown as ConstructorParameters<typeof MockLanguageModelV3>[0]),
prompt: 'Where is the first span?',
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export default Sentry.withSentry(
content: [{ type: 'text', text: 'Hello from mock AI!' }],
warnings: [],
}),
}),
// The mock result shape differs from this `ai` version's LanguageModelV3 result type; cast to the mock's expected config.
} as unknown as ConstructorParameters<typeof MockLanguageModelV3>[0]),
prompt: 'Where is the first span?',
});

Expand Down
4 changes: 4 additions & 0 deletions dev-packages/cloudflare-integration-tests/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

"include": ["suites/**/*.ts", "*.ts"],

// `suites/prisma` imports a Prisma-generated client (`./generated`) that only exists after codegen,
// so it can't be type-checked as part of the static suite check.
"exclude": ["suites/prisma/**"],

"compilerOptions": {
// Although this seems wrong to include `DOM` here, it's necessary to make
// global fetch available in tests in lower Node versions.
Expand Down
Loading