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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

### Important Changes

- **feat(node-core): Add node-core/light ([#18502](https://github.com/getsentry/sentry-javascript/pull/18502))**

This release adds a new light-weight `@sentry/node-core/light` export to `@sentry/node-core`. The export acts as a light-weight errors-only SDK that does not depend on OpenTelemetry.

Use this SDK when:
- You only need error tracking without performance monitoring
- You want to minimize bundle size and runtime overhead
- You don't need OpenTelemetry instrumentation

It supports basic error tracking and report, automatic request isolation (requires Node.js 22+) and basic tracing via our `Sentry.startSpan*` APIs.

Install the SDK by running

```bash
npm install @sentry/node-core
```

and add Sentry at the top of your application's entry file:

```js
import * as Sentry from '@sentry/node-core/light';

Sentry.init({
dsn: '__DSN__',
});
```

### Other Changes

- **feat(tanstackstart-react): Auto-instrument server function middleware ([#19001](https://github.com/getsentry/sentry-javascript/pull/19001))**

The `sentryTanstackStart` Vite plugin now automatically instruments middleware in `createServerFn().middleware([...])` calls. This captures performance data without requiring manual wrapping with `wrapMiddlewaresWithSentry()`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.env
pnpm-lock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "node-core-light-express-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/app.js",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/node-core": "latest || *",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"express": "^4.21.2",
"typescript": "~5.0.0"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "latest || *"
},
"volta": {
"node": "22.18.0"
},
"sentryTest": {
"variants": [
{
"label": "node 22 (light mode, requires Node 22+ for diagnostics_channel)"
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: 'pnpm start',
port: 3030,
});

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as Sentry from '@sentry/node-core/light';
import express from 'express';

// IMPORTANT: Initialize Sentry BEFORE creating the Express app
// This is required for automatic request isolation to work
Sentry.init({
dsn: process.env.E2E_TEST_DSN,
debug: true,
tracesSampleRate: 1.0,
tunnel: 'http://localhost:3031/', // Use event proxy for testing
});

// Create Express app AFTER Sentry.init()
const app = express();
const port = 3030;

app.get('/test-error', (_req, res) => {
Sentry.setTag('test', 'error');
Sentry.captureException(new Error('Test error from light mode'));
res.status(500).json({ error: 'Error captured' });
});

app.get('/test-isolation/:userId', async (req, res) => {
const userId = req.params.userId;

const isolationScope = Sentry.getIsolationScope();
const currentScope = Sentry.getCurrentScope();

Sentry.setUser({ id: userId });
Sentry.setTag('user_id', userId);

currentScope.setTag('processing_user', userId);
currentScope.setContext('api_context', {
userId,
timestamp: Date.now(),
});

// Simulate async work with variance so we run into cases where
// the next request comes in before the async work is complete
// to showcase proper request isolation
await new Promise(resolve => setTimeout(resolve, Math.random() * 500 + 100));

// Verify isolation after async operations
const finalIsolationData = isolationScope.getScopeData();
const finalCurrentData = currentScope.getScopeData();

const isIsolated =
finalIsolationData.user?.id === userId &&
finalIsolationData.tags?.user_id === userId &&
finalCurrentData.contexts?.api_context?.userId === userId;

res.json({
userId,
isIsolated,
scope: {
userId: finalIsolationData.user?.id,
userIdTag: finalIsolationData.tags?.user_id,
currentUserId: finalCurrentData.contexts?.api_context?.userId,
},
});
});

app.get('/test-isolation-error/:userId', (req, res) => {
const userId = req.params.userId;
Sentry.setTag('user_id', userId);
Sentry.setUser({ id: userId });

Sentry.captureException(new Error(`Error for user ${userId}`));
res.json({ userId, captured: true });
});

app.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});

app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'node-core-light-express',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('should capture errors', async ({ request }) => {
const errorEventPromise = waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Test error from light mode';
});

const response = await request.get('/test-error');
expect(response.status()).toBe(500);

const errorEvent = await errorEventPromise;
expect(errorEvent).toBeDefined();
expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from light mode');
expect(errorEvent.tags?.test).toBe('error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('should isolate scope data across concurrent requests', async ({ request }) => {
// Make 3 concurrent requests with different user IDs
const [response1, response2, response3] = await Promise.all([
request.get('/test-isolation/user-1'),
request.get('/test-isolation/user-2'),
request.get('/test-isolation/user-3'),
]);

const data1 = await response1.json();
const data2 = await response2.json();
const data3 = await response3.json();

// Each response should be properly isolated
expect(data1.isIsolated).toBe(true);
expect(data1.userId).toBe('user-1');
expect(data1.scope.userId).toBe('user-1');
expect(data1.scope.userIdTag).toBe('user-1');
expect(data1.scope.currentUserId).toBe('user-1');

expect(data2.isIsolated).toBe(true);
expect(data2.userId).toBe('user-2');
expect(data2.scope.userId).toBe('user-2');
expect(data2.scope.userIdTag).toBe('user-2');
expect(data2.scope.currentUserId).toBe('user-2');

expect(data3.isIsolated).toBe(true);
expect(data3.userId).toBe('user-3');
expect(data3.scope.userId).toBe('user-3');
expect(data3.scope.userIdTag).toBe('user-3');
expect(data3.scope.currentUserId).toBe('user-3');
});

test('should isolate errors across concurrent requests', async ({ request }) => {
const errorPromises = [
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-1';
}),
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-2';
}),
waitForError('node-core-light-express', event => {
return event?.exception?.values?.[0]?.value === 'Error for user user-3';
}),
];

// Make 3 concurrent requests that trigger errors
await Promise.all([
request.get('/test-isolation-error/user-1'),
request.get('/test-isolation-error/user-2'),
request.get('/test-isolation-error/user-3'),
]);

const [error1, error2, error3] = await Promise.all(errorPromises);

// Each error should have the correct user data
expect(error1?.user?.id).toBe('user-1');
expect(error1?.tags?.user_id).toBe('user-1');

expect(error2?.user?.id).toBe('user-2');
expect(error2?.tags?.user_id).toBe('user-2');

expect(error3?.user?.id).toBe('user-3');
expect(error3?.tags?.user_id).toBe('user-3');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"types": ["node"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Loading
Loading