Skip to content

Commit 2ae69d8

Browse files
committed
Merge remote-tracking branch 'origin' into jp/sync-master
2 parents c6a813e + c5b4468 commit 2ae69d8

19 files changed

Lines changed: 264 additions & 129 deletions

File tree

.agents/skills/write-tests/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,22 @@ test('captures transactions for all routes', async ({ baseURL }) => {
465465
});
466466
```
467467

468+
### SSR hydration timing
469+
470+
In SSR apps (Nuxt, SvelteKit, Next.js), elements are visible before framework event handlers attach.
471+
Playwright's actionability checks pass on server-rendered HTML, so `.click()` can fire before
472+
hydration, making it a no-op and timing out `waitForError`. Wait for the Sentry SDK before
473+
interacting:
474+
475+
```typescript
476+
await page.goto(`/test-param/1234`);
477+
await page.waitForFunction(() => typeof window.__SENTRY__ === 'object');
478+
await page.locator('#errorBtn').click();
479+
```
480+
481+
Don't use `networkidle`. Playwright discourages it. Use `waitForFunction` with a condition that
482+
directly proves readiness.
483+
468484
### Common pitfalls
469485

470486
- **Proxy name mismatch:** `APP_NAME` must match `proxyServerName` in `start-event-proxy.mjs`.

dev-packages/e2e-tests/test-applications/nitro-3/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@playwright/test": "~1.56.0",
2020
"@sentry-internal/test-utils": "link:../../../test-utils",
2121
"@sentry/core": "latest || *",
22-
"nitro": "^3.0.260429-beta",
22+
"nitro": "^3.0.260522-beta",
2323
"rolldown": "latest",
2424
"vite": "latest"
2525
},

dev-packages/e2e-tests/test-applications/nitro-3/tests/errors.test.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,16 @@ test('Sends an error event to Sentry', async ({ request }) => {
1010

1111
const errorEvent = await errorEventPromise;
1212

13-
// Nitro wraps thrown errors in an HTTPError with .cause, producing a chained exception
14-
expect(errorEvent.exception?.values).toHaveLength(2);
13+
expect(errorEvent.exception?.values).toHaveLength(1);
1514

16-
// The innermost exception (values[0]) is the original thrown error
1715
expect(errorEvent.exception?.values?.[0]?.type).toBe('Error');
1816
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is a test error');
1917
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
2018
expect.objectContaining({
2119
handled: false,
22-
type: 'auto.function.nitro.captureErrorHook',
20+
type: 'auto.http.nitro.onTraceError',
2321
}),
2422
);
25-
26-
// The outermost exception (values[1]) is the HTTPError wrapper
27-
expect(errorEvent.exception?.values?.[1]?.type).toBe('HTTPError');
28-
expect(errorEvent.exception?.values?.[1]?.value).toBe('This is a test error');
2923
});
3024

3125
test('Does not send 404 errors to Sentry', async ({ request }) => {

dev-packages/e2e-tests/test-applications/nitro-3/tests/span-nesting.test.ts

Lines changed: 29 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ test('Span nesting: h3 middleware spans are children of the srvx request span',
3333
expect(srvxSpan).toBeDefined();
3434

3535
// All h3 middleware spans should be children of the srvx span
36-
const h3Spans = event.spans?.filter(span => span.origin === 'auto.http.nitro.h3');
37-
expect(h3Spans?.length).toBeGreaterThanOrEqual(1);
36+
const h3MiddlewareSpans = event.spans?.filter(
37+
span => span.origin === 'auto.http.nitro.h3' && span.op === 'middleware.nitro',
38+
);
39+
expect(h3MiddlewareSpans?.length).toBeGreaterThanOrEqual(1);
3840

39-
for (const span of h3Spans ?? []) {
41+
for (const span of h3MiddlewareSpans ?? []) {
4042
expect(span.parent_span_id).toBe(srvxSpan!.span_id);
4143
}
4244
});
4345

44-
test('Span nesting: manual startSpan calls inside route handler are children of the srvx request span', async ({
45-
request,
46-
}) => {
46+
test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => {
4747
const transactionEventPromise = waitForTransaction('nitro-3', event => {
4848
return event?.transaction === 'GET /api/test-nesting';
4949
});
@@ -52,23 +52,38 @@ test('Span nesting: manual startSpan calls inside route handler are children of
5252

5353
const event = await transactionEventPromise;
5454

55-
// Find the srvx request span — this is the parent of all h3 and manual spans
5655
const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
5756
expect(srvxSpan).toBeDefined();
58-
const srvxSpanId = srvxSpan!.span_id;
57+
58+
const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server');
59+
expect(h3HandlerSpan).toBeDefined();
60+
expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id);
61+
});
62+
63+
test('Span nesting: manual startSpan calls inside route handler are children of the h3 route handler span', async ({
64+
request,
65+
}) => {
66+
const transactionEventPromise = waitForTransaction('nitro-3', event => {
67+
return event?.transaction === 'GET /api/test-nesting';
68+
});
69+
70+
await request.get('/api/test-nesting');
71+
72+
const event = await transactionEventPromise;
73+
74+
// Find the h3 route handler span
75+
const h3HandlerSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server');
76+
expect(h3HandlerSpan).toBeDefined();
5977

6078
// Find the manually created db spans
6179
const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
6280
const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
6381
expect(dbSelectSpan).toBeDefined();
6482
expect(dbInsertSpan).toBeDefined();
6583

66-
// FIXME: Once nitro's h3 tracing plugin emits a separate span for route handlers (type: "route"),
67-
// the db spans should be children of the h3 route handler span, not the srvx span directly.
68-
// Currently nitro bypasses h3's ~routes for file-based routing, so h3 only emits middleware spans.
69-
// Both db spans should be children of the srvx request span
70-
expect(dbSelectSpan!.parent_span_id).toBe(srvxSpanId);
71-
expect(dbInsertSpan!.parent_span_id).toBe(srvxSpanId);
84+
// Both db spans should be children of the h3 route handler span
85+
expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
86+
expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
7287

7388
// Both db spans should be siblings (same parent)
7489
expect(dbSelectSpan!.parent_span_id).toBe(dbInsertSpan!.parent_span_id);
@@ -79,49 +94,6 @@ test('Span nesting: manual startSpan calls inside route handler are children of
7994
expect(serializeSpan!.parent_span_id).toBe(dbInsertSpan!.span_id);
8095
});
8196

82-
// FIXME: Nitro's file-based routing bypasses h3's ~routes, so h3's tracing plugin never wraps
83-
// route handlers with type: "route". Once this is fixed upstream or we add our own wrapping,
84-
// uncomment these tests to verify the h3 route handler span exists and is the parent of manual spans.
85-
//
86-
// test('Span nesting: h3 route handler span is a child of the srvx request span', async ({ request }) => {
87-
// const transactionEventPromise = waitForTransaction('nitro-3', event => {
88-
// return event?.transaction === 'GET /api/test-nesting';
89-
// });
90-
//
91-
// await request.get('/api/test-nesting');
92-
//
93-
// const event = await transactionEventPromise;
94-
//
95-
// const srvxSpan = event.spans?.find(span => span.origin === 'auto.http.nitro.srvx' && span.op === 'http.server');
96-
// expect(srvxSpan).toBeDefined();
97-
//
98-
// const h3HandlerSpan = event.spans?.find(
99-
// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
100-
// );
101-
// expect(h3HandlerSpan).toBeDefined();
102-
// expect(h3HandlerSpan!.parent_span_id).toBe(srvxSpan!.span_id);
103-
// });
104-
//
105-
// test('Span nesting: manual startSpan calls are children of the h3 route handler span', async ({ request }) => {
106-
// const transactionEventPromise = waitForTransaction('nitro-3', event => {
107-
// return event?.transaction === 'GET /api/test-nesting';
108-
// });
109-
//
110-
// await request.get('/api/test-nesting');
111-
//
112-
// const event = await transactionEventPromise;
113-
//
114-
// const h3HandlerSpan = event.spans?.find(
115-
// span => span.origin === 'auto.http.nitro.h3' && span.op === 'http.server',
116-
// );
117-
// expect(h3HandlerSpan).toBeDefined();
118-
//
119-
// const dbSelectSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.select');
120-
// const dbInsertSpan = event.spans?.find(span => span.op === 'db' && span.description === 'db.insert');
121-
// expect(dbSelectSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
122-
// expect(dbInsertSpan!.parent_span_id).toBe(h3HandlerSpan!.span_id);
123-
// });
124-
12597
test('Span nesting: middleware spans start before manual spans in the span tree', async ({ request }) => {
12698
const transactionEventPromise = waitForTransaction('nitro-3', event => {
12799
return event?.transaction === 'GET /api/test-nesting';

dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.client.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test.describe('client-side errors', async () => {
3535
});
3636

3737
await page.goto(`/test-param/1234`);
38+
await page.waitForFunction(() => typeof window.__SENTRY__ === 'object');
3839
await page.locator('#errorBtn').click();
3940

4041
const error = await errorPromise;

dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"devDependencies": {
2121
"@playwright/test": "~1.56.0",
2222
"@sveltejs/adapter-cloudflare": "^5.0.3",
23-
"@sveltejs/kit": "2.52.2",
23+
"@sveltejs/kit": "2.60.1",
2424
"@sveltejs/vite-plugin-svelte": "^5.0.3",
2525
"svelte": "^5.20.2",
2626
"svelte-check": "^4.1.4",

packages/elysia/src/withElysia.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
winterCGRequestToRequestData,
1717
withIsolationScope,
1818
} from '@sentry/core';
19-
import type { Elysia, ErrorContext, TraceHandler, TraceListener } from 'elysia';
19+
import type { AnyElysia, Elysia, ErrorContext, TraceHandler, TraceListener } from 'elysia';
2020

2121
interface ElysiaHandlerOptions {
2222
shouldHandleError?: (context: ErrorContext) => boolean;
@@ -161,7 +161,12 @@ function instrumentLifecyclePhase(phaseName: string, listener: TraceListener, ro
161161
* .listen(3000);
162162
* ```
163163
*/
164-
export function withElysia<T extends Elysia>(app: T, options: ElysiaHandlerOptions = {}): T {
164+
// Using the AnyElysia type here to allow users to pass in the full set of Elysia
165+
// options without type errors. The `T extends Elysia` type is too narrow to allow
166+
// users to pass in dynamic options like `prefix` without breaking the type system.
167+
// See Elysia type definition and its usage of `const in out` which forces the string
168+
// template literals to be fully consistent for e.g. `prefix`.
169+
export function withElysia<T extends AnyElysia>(app: T, options: ElysiaHandlerOptions = {}): T {
165170
if (instrumentedApps.has(app)) {
166171
return app;
167172
}
@@ -275,7 +280,14 @@ export function withElysia<T extends Elysia>(app: T, options: ElysiaHandlerOptio
275280
});
276281
});
277282

278-
app.onAfterHandle({ as: 'global' }, function sentryOnAfterHandle(context) {
283+
// Cast from AnyElysia to Elysia so that onAfterHandle/onError callback
284+
// parameters resolve to typed contexts (e.g. ErrorContext for shouldHandleError).
285+
// `AnyElysia` is needed on the user-facing `app` type to accept user instances with
286+
// arbitrary generic args.
287+
// The cast is safe because AnyElysia IS Elysia<any, any, ...>.
288+
const elysiaApp = app as unknown as Elysia;
289+
290+
elysiaApp.onAfterHandle({ as: 'global' }, function sentryOnAfterHandle(context) {
279291
if (context.route) {
280292
updateRouteTransactionName(context.request, context.request.method, context.route);
281293
}
@@ -289,7 +301,7 @@ export function withElysia<T extends Elysia>(app: T, options: ElysiaHandlerOptio
289301
}
290302
});
291303

292-
app.onError({ as: 'global' }, function sentryOnError(context) {
304+
elysiaApp.onError({ as: 'global' }, function sentryOnError(context) {
293305
if (context.route) {
294306
updateRouteTransactionName(context.request, context.request.method, context.route);
295307
}

packages/react-router/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@opentelemetry/instrumentation": "^0.214.0",
5151
"@opentelemetry/semantic-conventions": "^1.40.0",
5252
"@sentry/browser": "10.54.0",
53-
"@sentry/cli": "^2.58.5",
53+
"@sentry/cli": "^2.58.6",
5454
"@sentry/core": "10.54.0",
5555
"@sentry/node": "10.54.0",
5656
"@sentry/react": "10.54.0",

packages/remix/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"@opentelemetry/instrumentation": "^0.214.0",
6969
"@opentelemetry/semantic-conventions": "^1.40.0",
7070
"@remix-run/router": "^1.23.2",
71-
"@sentry/cli": "^2.58.5",
71+
"@sentry/cli": "^2.58.6",
7272
"@sentry/core": "10.54.0",
7373
"@sentry/node": "10.54.0",
7474
"@sentry/react": "10.54.0",

packages/tanstackstart-react/src/client/sdk.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,16 @@ export function init(options: ReactBrowserOptions): Client | undefined {
1818
applyTunnelRouteOption(sentryOptions);
1919
applySdkMetadata(sentryOptions, 'tanstackstart-react', ['tanstackstart-react', 'react']);
2020

21+
sentryOptions.ignoreSpans = [
22+
...(sentryOptions.ignoreSpans || []),
23+
/\/node_modules\//,
24+
/\/favicon\.ico/,
25+
/\/@id\//,
26+
/\/@react-refresh/,
27+
/\/@tanstack-start\//,
28+
/\/@fs\//,
29+
/\/@vite\//,
30+
];
31+
2132
return initReactSDK(sentryOptions);
2233
}

0 commit comments

Comments
 (0)