Skip to content
Open
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: 2 additions & 1 deletion dev-packages/cloudflare-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"lint": "oxlint . --type-aware",
"lint:fix": "oxlint . --fix --type-aware",
"lint:types": "tsc --noEmit",
"lint:types": "tsc --noEmit && tsc --noEmit -p suites/types/tsconfig.v4.json && tsc --noEmit -p suites/types/tsconfig.v5.json && tsc --noEmit -p suites/types/typegen/tsconfig.v4.json && tsc --noEmit -p suites/types/typegen/tsconfig.v5.json",
"test": "vitest run",
"test:watch": "yarn test --watch"
},
Expand All @@ -31,6 +31,7 @@
"devDependencies": {
"@cloudflare/vite-plugin": "1.34.0",
"@cloudflare/workers-types": "^4.20260426.0",
"@cloudflare/workers-types-v5": "npm:@cloudflare/workers-types@5.20260710.1",
"@sentry-internal/test-utils": "10.67.0",
"@sentry/conventions": "0.16.0",
"eslint-plugin-regexp": "^3.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Type tests for `instrumentDurableObjectWithSentry`.
*
* The env of the options callback must be inferred from the Durable Object class —
* via its `DurableObject<Env>` base or an explicit constructor — and never collapse
* to `unknown`.
*/
import { DurableObject } from 'cloudflare:workers';
import { instrumentDurableObjectWithSentry } from '@sentry/cloudflare';
import { expectTypeOf } from 'vitest';

interface DoEnv {
SENTRY_DSN: string;
MY_DO: DurableObjectNamespace;
}

// ---------------------------------------------------------------------------
// 1. Env inferred from the `DurableObject<Env>` base class
// ---------------------------------------------------------------------------
class MyDurableObject extends DurableObject<DoEnv> {
async fetch(request: Request): Promise<Response> {
return new Response(request.url);
}
}

export const instrumentedDo = instrumentDurableObjectWithSentry(env => {
expectTypeOf(env).toEqualTypeOf<DoEnv>();
return { dsn: env.SENTRY_DSN };
}, MyDurableObject);

// The instrumented class keeps its type, including RPC methods and the namespace typing.
const _doClass: typeof MyDurableObject = instrumentedDo;

// ---------------------------------------------------------------------------
// 2. Explicit constructor with env annotation, bare base class
// ---------------------------------------------------------------------------
class MyDurableObjectCustomCtor extends DurableObject {
constructor(ctx: DurableObjectState, env: DoEnv) {
super(ctx, env);
}
}

export const instrumentedDoCustomCtor = instrumentDurableObjectWithSentry(env => {
expectTypeOf(env).toEqualTypeOf<DoEnv>();
return { dsn: env.SENTRY_DSN };
}, MyDurableObjectCustomCtor);

// ---------------------------------------------------------------------------
// 3. Explicit generic
// ---------------------------------------------------------------------------
export const instrumentedDoExplicit = instrumentDurableObjectWithSentry<DoEnv>(env => {
expectTypeOf(env).toEqualTypeOf<DoEnv>();
return { dsn: env.SENTRY_DSN };
}, MyDurableObject);

// ---------------------------------------------------------------------------
// 4. DurableObject<Env, Props>
// ---------------------------------------------------------------------------
interface DoProps {
shard: string;
}

class MyDurableObjectWithProps extends DurableObject<DoEnv, DoProps> {}

export const instrumentedDoWithProps = instrumentDurableObjectWithSentry(env => {
expectTypeOf(env).toEqualTypeOf<DoEnv>();
return { dsn: env.SENTRY_DSN };
}, MyDurableObjectWithProps);
42 changes: 42 additions & 0 deletions dev-packages/cloudflare-integration-tests/suites/types/misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Type tests for `sentryPagesPlugin` and `defineCloudflareOptions`.
*/
import { defineCloudflareOptions, sentryPagesPlugin } from '@sentry/cloudflare';
import { expectTypeOf } from 'vitest';

interface PagesEnv {
SENTRY_DSN: string;
}

// ---------------------------------------------------------------------------
// sentryPagesPlugin: explicit Env is typed, the default does not error
// ---------------------------------------------------------------------------
export const pagesPluginExplicit = sentryPagesPlugin<PagesEnv>(context => {
// `env` is `PagesEnv & { ASSETS: ... }` — the workers-types Pages intersection.
expectTypeOf(context.env.SENTRY_DSN).toEqualTypeOf<string>();
return { dsn: context.env.SENTRY_DSN };
});

export const pagesPluginDefault = sentryPagesPlugin(context => {
// No explicit generic: env access must not fail compilation.
return { dsn: context.env.SENTRY_DSN };
});

// ---------------------------------------------------------------------------
// defineCloudflareOptions: explicit Env is typed, the default does not error
// ---------------------------------------------------------------------------
interface OptionsEnv {
SENTRY_DSN: string;
}

export const optionsExplicit = defineCloudflareOptions<OptionsEnv>(env => {
expectTypeOf(env).toEqualTypeOf<OptionsEnv>();
return { dsn: env.SENTRY_DSN };
});

export const optionsDefault = defineCloudflareOptions(env => {
expectTypeOf(env).toBeAny();
return { dsn: env.SENTRY_DSN };
});

export const optionsStatic = defineCloudflareOptions({ tracesSampleRate: 1.0 });
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
// this tsconfig.json is only here so the IDE can find the types
// the actual lint:types goes against tsconfig.v4.json and tsconfig.v5.json directly
"extends": "./tsconfig.v5.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",

// The shared type tests in this folder run once per supported `@cloudflare/workers-types`
// major (see `./tsconfig.v5.json` for v5). The env inference machinery reads generic
// defaults off `ExportedHandler`, `WorkerEntrypoint`, `DurableObject` and
// `WorkflowEntrypoint`, so a change to any of those between majors has to fail here
// rather than in a user's project.
"include": ["./*.ts"],

// The extended config hands this whole folder to these per-version programs by excluding
// it; the inherited exclude must be cleared here or no inputs remain.
"exclude": [],

"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.v4.json",

// Same files as `./tsconfig.v4.json`, against `@cloudflare/workers-types` v5 (installed
// under the `@cloudflare/workers-types-v5` alias).
"compilerOptions": {
"types": ["@cloudflare/workers-types-v5"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "../../../tsconfig.json",

// Separate program: the global `Cloudflare.Env` augmentation in this folder simulates
// a project that ran `wrangler types`. It must not leak into the main program, where
// the no-typegen fallback (`env` resolves to `any`) is under test. Runs once per
// supported `@cloudflare/workers-types` major (see `./tsconfig.v5.json` for v5).
"include": ["./**/*.ts"],

// The extended config excludes this folder (to keep the augmentation out of the main
// program); since this program is exactly that folder, the inherited exclude must be
// cleared or no inputs remain.
"exclude": [],

"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.v4.json",

// Same files as `./tsconfig.v4.json`, against `@cloudflare/workers-types` v5 (installed
// under the `@cloudflare/workers-types-v5` alias).
"compilerOptions": {
"types": ["@cloudflare/workers-types-v5"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Type tests for the wrangler-generated (`wrangler types`) setup.
*
* This folder is a separate tsc program (see `./tsconfig.json`) so that the global
* `Cloudflare.Env` augmentation below simulates a project that ran `wrangler types`,
* without affecting the other suites: in the main program `Cloudflare.Env` stays the
* empty interface.
*/
import { DurableObject, WorkerEntrypoint, WorkflowEntrypoint } from 'cloudflare:workers';
import { instrumentDurableObjectWithSentry, instrumentWorkflowWithSentry, withSentry } from '@sentry/cloudflare';
import { expectTypeOf } from 'vitest';

// Simulates `wrangler types` output.
declare global {
namespace Cloudflare {
interface Env {
SENTRY_DSN: string;
MY_KV: KVNamespace;
}
}
}

// ---------------------------------------------------------------------------
// Bare handler, no annotations: env picks up the generated `Cloudflare.Env`
// ---------------------------------------------------------------------------
export const typegen = withSentry(
env => {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
return { dsn: env.SENTRY_DSN };
},
{
async fetch(request, env) {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
void env.MY_KV;
return new Response(request.url);
},
},
);

// ---------------------------------------------------------------------------
// Bare Durable Object (no generic): env picks up the generated `Cloudflare.Env`
// ---------------------------------------------------------------------------
class MyDurableObject extends DurableObject {}

export const instrumentedDo = instrumentDurableObjectWithSentry(env => {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
return { dsn: env.SENTRY_DSN };
}, MyDurableObject);

// ---------------------------------------------------------------------------
// Bare WorkerEntrypoint / WorkflowEntrypoint (no generic), the setup Cloudflare
// recommends: env picks up the generated `Cloudflare.Env`
// ---------------------------------------------------------------------------
class MyEntrypoint extends WorkerEntrypoint {}

export const entrypoint = withSentry(env => {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
return { dsn: env.SENTRY_DSN };
}, MyEntrypoint);

class MyWorkflow extends WorkflowEntrypoint {}

export const workflow = instrumentWorkflowWithSentry(env => {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
return { dsn: env.SENTRY_DSN };
}, MyWorkflow);

// ---------------------------------------------------------------------------
// Framework-wrapped handlers without their own env type (TanStack's
// `ServerEntry`): env picks up the generated `Cloudflare.Env`
// ---------------------------------------------------------------------------
type ServerEntry = {
fetch: (request: Request, opts?: unknown) => Promise<Response> | Response;
};

declare const serverEntry: ServerEntry;

export const tanstack = withSentry(env => {
expectTypeOf(env).toEqualTypeOf<Cloudflare.Env>();
return { dsn: env.SENTRY_DSN };
}, serverEntry);
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Type tests for `ExportedHandler`'s 4th generic — `Props` for `ExecutionContext<Props>`.
*
* A `Props`-typed handler must still be accepted by `withSentry`, keep its exact type,
* and have its env inferred. Runs in every program, since both `@cloudflare/workers-types`
* v4 and v5 carry the `Props` generic.
*/
import { withSentry } from '@sentry/cloudflare';
import { expectTypeOf } from 'vitest';

interface PropsEnv {
SENTRY_DSN: string;
}

interface MyProps {
jobId: string;
}

const propsHandler: ExportedHandler<PropsEnv, unknown, unknown, MyProps> = {
async fetch(_, env, ctx) {
expectTypeOf(ctx.props.jobId).toEqualTypeOf<string>();
void env.SENTRY_DSN;
return new Response('ok');
},
};

// A pre-typed handler with `ExecutionContext<Props>` is accepted and its env inferred.
export const props = withSentry(env => {
expectTypeOf(env).toEqualTypeOf<PropsEnv>();
return { dsn: env.SENTRY_DSN };
}, propsHandler);

// The wrapped handler keeps its exact type, including the Props.
expectTypeOf(props).toEqualTypeOf<ExportedHandler<PropsEnv, unknown, unknown, MyProps>>();

// A `satisfies` handler literal with `ExecutionContext<Props>` works too.
export const propsSatisfies = withSentry(
env => {
expectTypeOf(env).toEqualTypeOf<PropsEnv>();
return { dsn: env.SENTRY_DSN };
},
{
async fetch(_, env, ctx) {
expectTypeOf(env).toEqualTypeOf<PropsEnv>();
expectTypeOf(ctx.props.jobId).toEqualTypeOf<string>();
void env.SENTRY_DSN;
return new Response('ok');
},
} satisfies ExportedHandler<PropsEnv, unknown, unknown, MyProps>,
);
Loading
Loading