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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as Sentry from '@sentry/bun';

Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' });

// eslint-disable-next-line no-console
console.log('Sentry initialized');
32 changes: 32 additions & 0 deletions dev-packages/bun-integration-tests/suites/bundled-app/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { execFileSync, spawnSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterAll, expect, test } from 'vitest';

// Outside the repo, so no `node_modules` is above the bundle, as in a deployed app.
const runDir = mkdtempSync(join(tmpdir(), 'sentry-bun-bundled-app-'));

afterAll(() => {
rmSync(runDir, { recursive: true, force: true });
});

// Without `node_modules`, a module lookup starts Bun's auto-install, which exits the process when it
// cannot create its cache: https://github.com/getsentry/sentry-javascript/issues/25540
test('a bundled app initializes when Bun cannot create its package cache', () => {
const app = join(runDir, 'app.mjs');
execFileSync('bun', ['build', join(__dirname, 'index.ts'), '--target=bun', '--format=esm', `--outfile=${app}`]);
// Files block the cache directories. Unlike file permissions, this also works for root.
writeFileSync(join(runDir, 'node_modules'), '');
writeFileSync(join(runDir, 'cache'), '');

const result = spawnSync('bun', [app], {
cwd: runDir,
encoding: 'utf8',
env: { ...process.env, BUN_INSTALL_CACHE_DIR: join(runDir, 'cache', 'bun') },
});

expect(result.stderr).not.toContain('unable to write files');
expect(result.stdout).toContain('Sentry initialized');
expect(result.status).toBe(0);
});
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,9 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
});

test('error from failed internal fetch is correlated with the storefront trace', async ({ baseURL }) => {
// Not `ghost`: Dedupe would drop this error as a repeat of the error in the test above.
const errorPromise = waitForError(APP_NAME, event => {
return event.exception?.values?.[0]?.value === 'Failed to fetch product: ghost';
return event.exception?.values?.[0]?.value === 'Failed to fetch product: phantom';
});

const segmentPromise = waitForStreamedSpan(
Expand All @@ -372,7 +373,7 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
segment.name === `GET ${STOREFRONT}/product-or-throw/:productId`,
);

await fetch(`${baseURL}${STOREFRONT}/product-or-throw/ghost`);
await fetch(`${baseURL}${STOREFRONT}/product-or-throw/phantom`);

const [errorEvent, segment] = await Promise.all([errorPromise, segmentPromise]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ test('attaches HTTP connection info to the server span', async ({ baseURL, page
expect(data['client.address']?.value).toEqual(expect.any(String));
expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);

if (RUNTIME !== 'deno') {
// Only exposed in `hono/deno`
if (RUNTIME !== 'node' && RUNTIME !== 'deno') {
// Only the Node HTTP server span and Deno's `remoteAddr` give a transport.
expect(data['network.transport']?.value).toBeUndefined();
} else {
expect(data['network.transport']?.value).toMatch(/tcp/);
Expand Down Expand Up @@ -99,7 +99,7 @@ test("preserves the baseline server.*, client.* and network.* server span attrib
expect(data['network.type']?.value).toMatch(/^ipv[46]$/);
expect(data['network.protocol.name']?.value).toBe('http');
expect(data['network.protocol.version']?.value).toBe('1.1');
expect(data['network.transport']?.value).toBeUndefined();
expect(data['network.transport']?.value).toBe('tcp');
expect(data['network.local.port']?.value).toBe(data['server.port']?.value);
expect(data['network.local.address']?.value).toEqual(expect.any(String));
expect(data['network.peer.address']?.value).toBe(data['client.address']?.value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,7 @@ test.describe('multi-fetch: internal .request() calls between sub-apps', () => {
});

test('error from failed internal fetch is correlated with the storefront trace', async ({ baseURL }) => {
// Use a param unique to this test: the `no error status` test above fetches `/ghost` too, and its
// identical `Failed to fetch product: ghost` error would otherwise be dropped by the Dedupe
// integration, so this test's `waitForError` would never fire.
// Not `ghost`: Dedupe would drop this error as a repeat of the error in the test above.
const errorPromise = waitForError(APP_NAME, event => {
return event.exception?.values?.[0]?.value === 'Failed to fetch product: phantom';
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const anyString = expect.any(String) as unknown;
const anyNumber = expect.any(Number) as unknown;
const ipvType = expect.stringMatching(/^ipv[46]$/) as unknown;

// Per-runtime `http.server` connection-info expectations. Each runtime's `getConnInfo` helper exposes
// Per-runtime `http.server` connection-info expectations. Each runtime exposes
// a different set of fields, so the expected attribute values are declared here once (keyed by
// runtime) instead of branching inside the tests. `undefined` means the attribute must be absent.
// Relational checks (`network.peer.*` mirroring `client.*`) are asserted in the tests, since they
Expand All @@ -21,7 +21,7 @@ const CONN_INFO: Record<Runtime, { added: Record<string, unknown>; baseline: Rec
'client.address': anyString,
'client.port': anyNumber,
'network.type': ipvType,
'network.transport': undefined,
'network.transport': 'tcp',
},
baseline: {
'server.address': 'localhost',
Expand All @@ -30,7 +30,7 @@ const CONN_INFO: Record<Runtime, { added: Record<string, unknown>; baseline: Rec
'network.type': ipvType,
'network.protocol.name': 'http',
'network.protocol.version': '1.1',
'network.transport': undefined,
'network.transport': 'tcp',
'network.local.address': anyString,
'network.local.port': anyNumber,
},
Expand All @@ -45,7 +45,7 @@ const CONN_INFO: Record<Runtime, { added: Record<string, unknown>; baseline: Rec
baseline: { 'client.port': anyNumber, 'network.type': ipvType },
},
deno: {
// Only `hono/deno` exposes `network.transport`.
// `network.transport` comes from Deno's `remoteAddr`.
added: { 'client.address': anyString, 'client.port': anyNumber, 'network.transport': expect.stringMatching(/tcp/) },
baseline: {
'server.address': 'localhost',
Expand All @@ -55,11 +55,9 @@ const CONN_INFO: Record<Runtime, { added: Record<string, unknown>; baseline: Rec
},
},
cloudflare: {
// Cloudflare Workers expose no client address, port, address family, or transport: there is no
// `getConnInfo` on Workers and nothing else populates `client.address`, so all of these are
// absent. Asserting `undefined` here lets us notice if that ever changes.
// Workers give only the client address (the `cf-connecting-ip` header).
added: {
'client.address': undefined,
'client.address': anyString,
'client.port': undefined,
'network.type': undefined,
'network.transport': undefined,
Expand Down
5 changes: 0 additions & 5 deletions packages/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
},
"peerDependencies": {
"@cloudflare/workers-types": "^4.x || ^5.x",
"@hono/node-server": "^1.x || ^2.x",
"@sentry/bun": "11.8.0",
"@sentry/cloudflare": "11.8.0",
"@sentry/deno": "11.8.0",
Expand All @@ -90,9 +89,6 @@
"@cloudflare/workers-types": {
"optional": true
},
"@hono/node-server": {
"optional": true
},
"@sentry/bun": {
"optional": true
},
Expand All @@ -108,7 +104,6 @@
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250922.0",
"@hono/node-server": "^2.0.10",
"@types/node": "^20.19.0",
"wrangler": "4.62.0"
},
Expand Down
3 changes: 1 addition & 2 deletions packages/hono/src/bun/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { BunOptions } from '@sentry/bun';
import { debug } from '@sentry/core';
import { init } from './sdk';
import { getConnInfo } from 'hono/bun';
import { applyHonoPatches, createHonoRequestMiddleware, type SentryHonoMiddlewareOptions } from '@sentry/server-utils';
import type { Env, Hono, MiddlewareHandler } from 'hono';

Expand All @@ -17,5 +16,5 @@ export const sentry = <E extends Env>(app: Hono<E>, options: HonoBunOptions): Mi

applyHonoPatches(app);

return createHonoRequestMiddleware({ getConnInfo, shouldHandleError: options.shouldHandleError });
return createHonoRequestMiddleware({ runtime: 'bun', shouldHandleError: options.shouldHandleError });
};
3 changes: 1 addition & 2 deletions packages/hono/src/cloudflare/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { withSentry, type CloudflareOptions } from '@sentry/cloudflare';
import { applySdkMetadata, debug } from '@sentry/core';
import { getConnInfo } from 'hono/cloudflare-workers';
import {
applyHonoPatches,
createHonoRequestMiddleware,
Expand Down Expand Up @@ -46,7 +45,7 @@ export function sentry<E extends Env>(
applyHonoPatches(app);

return createHonoRequestMiddleware({
getConnInfo,
runtime: 'cloudflare',
// Cloudflare accepts middleware options as a function of `env`, so `shouldHandleError` is only
// known per request.
resolveShouldHandleError: context =>
Expand Down
3 changes: 1 addition & 2 deletions packages/hono/src/deno/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { debug } from '@sentry/core';
import type { DenoOptions } from '@sentry/deno';
import { init } from './sdk';
import { getConnInfo } from 'hono/deno';
import {
applyHonoPatches,
createHonoRequestMiddleware,
Expand All @@ -21,5 +20,5 @@ export const sentry = <E extends Env>(app: Hono<E>, options: HonoDenoOptions): M

applyHonoPatches(app);

return createHonoRequestMiddleware({ getConnInfo, shouldHandleError: options.shouldHandleError });
return createHonoRequestMiddleware({ runtime: 'deno', shouldHandleError: options.shouldHandleError });
};
3 changes: 1 addition & 2 deletions packages/hono/src/node/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { consoleSandbox, debug, getClient } from '@sentry/core';
import type { NodeOptions } from '@sentry/node';
import { getConnInfo } from '@hono/node-server/conninfo';
import { applyHonoPatches, createHonoRequestMiddleware, type SentryHonoMiddlewareOptions } from '@sentry/server-utils';
import type { Env, Hono, MiddlewareHandler } from 'hono';

Expand Down Expand Up @@ -41,5 +40,5 @@ export const sentry = <E extends Env>(app: Hono<E>, options?: SentryHonoMiddlewa

applyHonoPatches(app);

return createHonoRequestMiddleware({ getConnInfo, shouldHandleError: options?.shouldHandleError });
return createHonoRequestMiddleware({ runtime: 'node', shouldHandleError: options?.shouldHandleError });
};
32 changes: 26 additions & 6 deletions packages/hono/test/bun/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {
CLIENT_ADDRESS,
CLIENT_PORT,
NETWORK_PEER_ADDRESS,
NETWORK_PEER_PORT,
NETWORK_TRANSPORT,
NETWORK_TYPE,
} from '@sentry/conventions/attributes';
import * as SentryCore from '@sentry/core';
import { SDK_VERSION } from '@sentry/core';
import { Hono } from 'hono';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { sentry, type HonoBunOptions } from '../../src/bun/middleware';
import { init } from '../../src/bun/sdk';
import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../../src/shared/lowQualityTransactionPatterns';
import { requestConnInfoAttributes } from '../shared/requestConnInfoAttributes';

vi.mock('@sentry/bun', () => ({
init: vi.fn(),
}));

// `hono/bun` eagerly imports Bun-only modules (e.g. SSG) that reference the `Bun` global,
// which is not available under Vitest/Node. We only use its `getConnInfo` helper.
vi.mock('hono/bun', () => ({
getConnInfo: vi.fn(() => ({ remote: {} })),
}));

// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const { init: initBunMock } = await vi.importMock<typeof import('@sentry/bun')>('@sentry/bun');

Expand Down Expand Up @@ -254,3 +257,20 @@ describe('Hono Bun Middleware', () => {
});
});
});

describe('Hono Bun Middleware — connection info', () => {
it('reads the client address from `requestIP` of the server', async () => {
const middleware = sentry(new Hono(), { dsn: 'https://public@dsn.ingest.sentry.io/1337' });
// `Bun.serve` passes the server to `app.fetch` as `env`.
const env = { requestIP: () => ({ address: '::1', port: 54321, family: 'IPv6' }) };

const attributes = await requestConnInfoAttributes(middleware, { env });

expect(attributes[CLIENT_ADDRESS]).toBe('::1');
expect(attributes[CLIENT_PORT]).toBe(54321);
expect(attributes[NETWORK_PEER_ADDRESS]).toBe('::1');
expect(attributes[NETWORK_PEER_PORT]).toBe(54321);
expect(attributes[NETWORK_TYPE]).toBe('ipv6');
expect(attributes).not.toHaveProperty(NETWORK_TRANSPORT);
});
});
25 changes: 25 additions & 0 deletions packages/hono/test/cloudflare/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import {
CLIENT_ADDRESS,
CLIENT_PORT,
NETWORK_PEER_ADDRESS,
NETWORK_PEER_PORT,
NETWORK_TRANSPORT,
NETWORK_TYPE,
} from '@sentry/conventions/attributes';
import * as SentryCloudflare from '@sentry/cloudflare';
import * as SentryCore from '@sentry/core';
import { SDK_VERSION } from '@sentry/core';
import { Hono } from 'hono';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { sentry, type HonoCloudflareOptions } from '../../src/cloudflare/middleware';
import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../../src/shared/lowQualityTransactionPatterns';
import { requestConnInfoAttributes } from '../shared/requestConnInfoAttributes';

vi.mock('@sentry/cloudflare', { spy: true });
vi.mock('@sentry/core', async () => {
Expand Down Expand Up @@ -197,3 +206,19 @@ describe('Hono Cloudflare Middleware', () => {
});
});
});

describe('Hono Cloudflare Middleware — connection info', () => {
it('reads the `cf-connecting-ip` header', async () => {
const middleware = sentry(new Hono(), { dsn: 'https://public@dsn.ingest.sentry.io/1337' });
const headers = { 'cf-connecting-ip': '203.0.113.5' };

const attributes = await requestConnInfoAttributes(middleware, { headers });

expect(attributes[CLIENT_ADDRESS]).toBe('203.0.113.5');
expect(attributes[NETWORK_PEER_ADDRESS]).toBe('203.0.113.5');
expect(attributes).not.toHaveProperty(CLIENT_PORT);
expect(attributes).not.toHaveProperty(NETWORK_PEER_PORT);
expect(attributes).not.toHaveProperty(NETWORK_TYPE);
expect(attributes).not.toHaveProperty(NETWORK_TRANSPORT);
});
});
32 changes: 26 additions & 6 deletions packages/hono/test/deno/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import {
CLIENT_ADDRESS,
CLIENT_PORT,
NETWORK_PEER_ADDRESS,
NETWORK_PEER_PORT,
NETWORK_TRANSPORT,
NETWORK_TYPE,
} from '@sentry/conventions/attributes';
import * as SentryCore from '@sentry/core';
import { SDK_VERSION } from '@sentry/core';
import { Hono } from 'hono';
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
import { sentry, type HonoDenoOptions } from '../../src/deno/middleware';
import { init } from '../../src/deno/sdk';
import { LOW_QUALITY_TRANSACTION_PATTERNS } from '../../src/shared/lowQualityTransactionPatterns';
import { requestConnInfoAttributes } from '../shared/requestConnInfoAttributes';

vi.mock('@sentry/deno', () => ({
init: vi.fn(),
}));

// `hono/deno` references the `Deno` global at module-eval time, which is not defined in the
// vitest (Node) environment.
vi.mock('hono/deno', () => ({
getConnInfo: vi.fn(),
}));

// eslint-disable-next-line @typescript-eslint/consistent-type-imports
const { init: initDenoMock } = await vi.importMock<typeof import('@sentry/deno')>('@sentry/deno');

Expand Down Expand Up @@ -254,3 +257,20 @@ describe('Hono Deno Middleware', () => {
});
});
});

describe('Hono Deno Middleware — connection info', () => {
it('reads the `remoteAddr` of the serve handler info', async () => {
const middleware = sentry(new Hono(), { dsn: 'https://public@dsn.ingest.sentry.io/1337' });
// `Deno.serve` passes the `ServeHandlerInfo` to `app.fetch` as `env`.
const env = { remoteAddr: { hostname: '127.0.0.1', port: 54321, transport: 'tcp' } };

const attributes = await requestConnInfoAttributes(middleware, { env });

expect(attributes[CLIENT_ADDRESS]).toBe('127.0.0.1');
expect(attributes[CLIENT_PORT]).toBe(54321);
expect(attributes[NETWORK_PEER_ADDRESS]).toBe('127.0.0.1');
expect(attributes[NETWORK_PEER_PORT]).toBe(54321);
expect(attributes[NETWORK_TRANSPORT]).toBe('tcp');
expect(attributes).not.toHaveProperty(NETWORK_TYPE);
});
});
Loading
Loading