Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
be0c75d
Merge pull request #17757 from getsentry/master
github-actions[bot] Sep 24, 2025
ae747ad
test(e2e): Pin `import-in-the-middle@1.14.2` due to `@vercel/nft` inc…
Lms24 Sep 25, 2025
4df0621
ref(aws-serverless): Add resolution for `import-in-the-middle` when b…
Lms24 Sep 25, 2025
450d9f5
build(aws): Ensure AWS build cache does not keep old files (#17776)
mydea Sep 25, 2025
f4df972
fix(core): Prevent `instrumentAnthropicAiClient` breaking MessageStre…
andreiborza Sep 25, 2025
42604ea
feat(react-router): Update loadContext type to be compatible with mid…
chargome Sep 25, 2025
930c1f0
Revert "test(e2e): Pin `import-in-the-middle@1.14.2` due to `@vercel/…
Lms24 Sep 25, 2025
b29c880
ref(aws-serverless): Improve README with better examples (#17787)
andreiborza Sep 25, 2025
80e26e0
feat(replay/logs): Only attach sampled replay Ids to logs (#17750)
chargome Sep 26, 2025
48882d2
test(react-router): Test v8 middleware (#17783)
chargome Sep 26, 2025
8424fdc
test(nextjs): Add route handler tests for turbopack (#17515)
chargome Sep 26, 2025
80d5ff2
feat(logs): Add internal `replay_is_buffering` flag (#17752)
chargome Sep 26, 2025
0e19673
ref(core): Improve promise buffer (#17788)
mydea Sep 26, 2025
0b4a7b1
chore: Add `publish_release` command (#17797)
chargome Sep 29, 2025
22f8e5a
fix(react): Do not send additional navigation span on pageload (#17799)
s1gr1d Sep 29, 2025
5ce4435
fix(nextjs): Don't use chalk in turbopack config file (#17806)
chargome Sep 29, 2025
c94d2a9
fix(browser): Use current start timestamp for CLS span when CLS is 0 …
Lms24 Sep 29, 2025
55d8514
meta(changelog): Update changelog for 10.16.0
s1gr1d Sep 29, 2025
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
5 changes: 5 additions & 0 deletions .cursor/commands/publish_release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Release Command

Execute the standard Sentry JavaScript SDK release process.

Find the "publishing_release" rule in `.cursor/rules/publishing_release` and follow those complete instructions step by step.
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@

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

## 10.16.0

- feat(logs): Add internal `replay_is_buffering` flag ([#17752](https://github.com/getsentry/sentry-javascript/pull/17752))
- feat(react-router): Update loadContext type to be compatible with middleware ([#17758](https://github.com/getsentry/sentry-javascript/pull/17758))
- feat(replay/logs): Only attach sampled replay Ids to logs ([#17750](https://github.com/getsentry/sentry-javascript/pull/17750))
- fix(browser): Use current start timestamp for CLS span when CLS is 0 ([#17800](https://github.com/getsentry/sentry-javascript/pull/17800))
- fix(core): Prevent `instrumentAnthropicAiClient` breaking MessageStream api ([#17754](https://github.com/getsentry/sentry-javascript/pull/17754))
- fix(nextjs): Don't use chalk in turbopack config file ([#17806](https://github.com/getsentry/sentry-javascript/pull/17806))
- fix(react): Do not send additional navigation span on pageload ([#17799](https://github.com/getsentry/sentry-javascript/pull/17799))

<details>
<summary> <strong>Internal Changes</strong> </summary>

- build(aws): Ensure AWS build cache does not keep old files ([#17776](https://github.com/getsentry/sentry-javascript/pull/17776))
- chore: Add `publish_release` command ([#17797](https://github.com/getsentry/sentry-javascript/pull/17797))
- ref(aws-serverless): Add resolution for `import-in-the-middle` when building the Lambda layer ([#17780](https://github.com/getsentry/sentry-javascript/pull/17780))
- ref(aws-serverless): Improve README with better examples ([#17787](https://github.com/getsentry/sentry-javascript/pull/17787))
- ref(core): Improve promise buffer ([#17788](https://github.com/getsentry/sentry-javascript/pull/17788))
- Revert "test(e2e): Pin `import-in-the-middle@1.14.2` due to `@vercel/nft` incompatibility ([#17777](https://github.com/getsentry/sentry-javascript/pull/17777))" (#17784)
- test(e2e): Pin `import-in-the-middle@1.14.2` due to `@vercel/nft` incompatibility ([#17777](https://github.com/getsentry/sentry-javascript/pull/17777))
- test(nextjs): Add route handler tests for turbopack ([#17515](https://github.com/getsentry/sentry-javascript/pull/17515))
- test(react-router): Test v8 middleware ([#17783](https://github.com/getsentry/sentry-javascript/pull/17783))

</details>

## 10.15.0

### Important Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET(request: Request) {
throw new Error('Dynamic route handler error');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json({ name: 'Beep' });
}

export async function POST() {
return NextResponse.json({ name: 'Boop' }, { status: 400 });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
return NextResponse.json({ name: 'Static' });
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@types/node": "^18.19.1",
"@types/react": "^19",
"@types/react-dom": "^19",
"next": "^15.3.5",
"next": "^15.5.4",
"react": "^19",
"react-dom": "^19",
"typescript": "~5.0.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';

test('Should create a transaction for dynamic route handlers', async ({ request }) => {
const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
return transactionEvent?.transaction === 'GET /route-handlers/[param]';
});

const response = await request.get('/route-handlers/foo');
expect(await response.json()).toStrictEqual({ name: 'Beep' });

const routehandlerTransaction = await routehandlerTransactionPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
});

test('Should create a transaction for static route handlers', async ({ request }) => {
const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
return transactionEvent?.transaction === 'GET /route-handlers/static';
});

const response = await request.get('/route-handlers/static');
expect(await response.json()).toStrictEqual({ name: 'Static' });

const routehandlerTransaction = await routehandlerTransactionPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
});

test('Should create a transaction for route handlers and correctly set span status depending on http status', async ({
request,
}) => {
const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
return transactionEvent?.transaction === 'POST /route-handlers/[param]';
});

const response = await request.post('/route-handlers/bar');
expect(await response.json()).toStrictEqual({ name: 'Boop' });

const routehandlerTransaction = await routehandlerTransactionPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('invalid_argument');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
});

test('Should record exceptions and transactions for faulty route handlers', async ({ request }) => {
const errorEventPromise = waitForError('nextjs-turbo', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'Dynamic route handler error';
});

const routehandlerTransactionPromise = waitForTransaction('nextjs-turbo', async transactionEvent => {
return transactionEvent?.transaction === 'GET /route-handlers/[param]/error';
});

await request.get('/route-handlers/boop/error').catch(() => {});

const routehandlerTransaction = await routehandlerTransactionPromise;
const routehandlerError = await errorEventPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('internal_error');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
expect(routehandlerTransaction.contexts?.trace?.origin).toContain('auto');

expect(routehandlerError.exception?.values?.[0].value).toBe('Dynamic route handler error');

expect(routehandlerError.request?.method).toBe('GET');
// todo: make sure url is attached to request object
// expect(routehandlerError.request?.url).toContain('/route-handlers/boop/error');

expect(routehandlerError.transaction).toBe('/route-handlers/[param]/error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from 'react-router';

export type User = {
id: string;
name: string;
};

export const userContext = createContext<User | null>(null);
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ export default [
route('static', 'routes/performance/static.tsx'),
route('server-loader', 'routes/performance/server-loader.tsx'),
route('server-action', 'routes/performance/server-action.tsx'),
route('with-middleware', 'routes/performance/with-middleware.tsx'),
]),
] satisfies RouteConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Route } from './+types/with-middleware';
import type { User } from '../../context';
import { userContext } from '../../context';
import * as Sentry from '@sentry/react-router';

async function getUser() {
await new Promise(resolve => setTimeout(resolve, 500));
return {
id: '1',
name: 'Carlos Gomez',
};
}

const authMiddleware: Route.MiddlewareFunction = async ({ request, context }, next) => {
Sentry.startSpan({ name: 'authMiddleware', op: 'middleware.auth' }, async () => {
const user: User = await getUser();
context.set(userContext, user);
await next();
});
};

export const middleware: Route.MiddlewareFunction[] = [authMiddleware];

export const loader = async ({ context }: Route.LoaderArgs) => {
const user = context.get(userContext);
return { user };
};

export default function WithMiddlewarePage({ loaderData }: Route.ComponentProps) {
const { user } = loaderData;

return (
<div>
<h1>With Middleware Page</h1>
<p>User: {user?.name}</p>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"scripts": {
"build": "react-router build",
"test:build-latest": "pnpm install && pnpm add react-router@latest && pnpm add @react-router/node@latest && pnpm add @react-router/serve@latest && pnpm build",
"dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev",
"start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js",
"proxy": "node start-event-proxy.mjs",
Expand Down Expand Up @@ -54,5 +55,13 @@
},
"volta": {
"extends": "../../package.json"
},
"sentryTest": {
"variants": [
{
"build-command": "pnpm test:build-latest",
"label": "react-router-7-framework (latest)"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ import type { Config } from '@react-router/dev/config';
export default {
ssr: true,
prerender: ['/performance/static'],
future: {
v8_middleware: true,
},
} satisfies Config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { APP_NAME } from '../constants';

test.describe('server - middleware', () => {
test('should send middleware transaction on pageload', async ({ page }) => {
const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'GET /performance/with-middleware';
});

const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === '/performance/with-middleware';
});

const customMiddlewareTxPromise = waitForTransaction(APP_NAME, async transactionEvent => {
return transactionEvent.transaction === 'authMiddleware';
});

await page.goto(`/performance/with-middleware`);

const serverTx = await serverTxPromise;
const pageloadTx = await pageloadTxPromise;
const customMiddlewareTx = await customMiddlewareTxPromise;

const traceIds = {
server: serverTx?.contexts?.trace?.trace_id,
pageload: pageloadTx?.contexts?.trace?.trace_id,
customMiddleware: customMiddlewareTx?.contexts?.trace?.trace_id,
};

expect(pageloadTx).toBeDefined();
expect(customMiddlewareTx).toBeDefined();

// Assert that all transactions belong to the same trace
expect(traceIds.server).toBe(traceIds.pageload);
expect(traceIds.server).toBe(traceIds.customMiddleware);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,37 @@ test('Creates a pageload transaction with parameterized route', async ({ page })
expect(event.contexts?.trace?.op).toBe('pageload');
});

test('Does not create a navigation transaction on initial load to deep lazy route', async ({ page }) => {
const navigationPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
});

const pageloadPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
return (
!!transactionEvent?.transaction &&
transactionEvent.contexts?.trace?.op === 'pageload' &&
transactionEvent.transaction === '/lazy/inner/:id/:anotherId/:someAnotherId'
);
});

await page.goto('/lazy/inner/1/2/3');

const pageloadEvent = await pageloadPromise;

expect(pageloadEvent.transaction).toBe('/lazy/inner/:id/:anotherId/:someAnotherId');

const lazyRouteContent = page.locator('id=innermost-lazy-route');
await expect(lazyRouteContent).toBeVisible();

// "Race" between navigation transaction and a timeout to ensure no navigation transaction is created within the timeout period
const result = await Promise.race([
navigationPromise.then(() => 'navigation'),
new Promise<'timeout'>(resolve => setTimeout(() => resolve('timeout'), 1500)),
]);

expect(result).toBe('timeout');
});

test('Creates a navigation transaction inside a lazy route', async ({ page }) => {
const transactionPromise = waitForTransaction('react-router-7-lazy-routes', async transactionEvent => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,55 @@ function startMockAnthropicServer() {
return;
}

// Check if streaming is requested
if (req.body.stream === true) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});

// Send streaming events
const events = [
{
type: 'message_start',
message: {
id: 'msg_stream123',
type: 'message',
role: 'assistant',
model,
content: [],
usage: { input_tokens: 10 },
},
},
{ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } },
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'Hello ' } },
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'from ' } },
{ type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'stream!' } },
{ type: 'content_block_stop', index: 0 },
{
type: 'message_delta',
delta: { stop_reason: 'end_turn', stop_sequence: null },
usage: { output_tokens: 15 },
},
{ type: 'message_stop' },
];

events.forEach((event, index) => {
setTimeout(() => {
res.write(`event: ${event.type}\n`);
res.write(`data: ${JSON.stringify(event)}\n\n`);

if (index === events.length - 1) {
res.end();
}
}, index * 10); // Small delay between events
});

return;
}

// Non-streaming response
res.send({
id: 'msg_mock123',
type: 'message',
Expand Down Expand Up @@ -92,8 +141,32 @@ async function run() {

// Fourth test: models.retrieve
await client.models.retrieve('claude-3-haiku-20240307');

// Fifth test: streaming via messages.create
const stream = await client.messages.create({
model: 'claude-3-haiku-20240307',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
stream: true,
});

for await (const _ of stream) {
void _;
}

// Sixth test: streaming via messages.stream
await client.messages
.stream({
model: 'claude-3-haiku-20240307',
messages: [{ role: 'user', content: 'What is the capital of France?' }],
})
.on('streamEvent', () => {
Sentry.captureMessage('stream event from user-added event listener captured');
});
});

// Wait for the stream event handler to finish
await Sentry.flush(2000);

server.close();
}

Expand Down
Loading
Loading