Skip to content

feat(nextjs): Instrument app dir route handlers #7369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 28 commits into from
Closed
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
1 change: 1 addition & 0 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
* @inheritDoc
*/
public flush(timeout?: number): PromiseLike<boolean> {
__DEBUG_BUILD__ && logger.warn('Flushing events.');
const transport = this._transport;
if (transport) {
return this._isClientDoneProcessing(timeout).then(clientFinished => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ next-env.d.ts
.sentryclirc

.vscode
test-results
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'GET' });
}

export async function POST() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'POST' });
}

export async function PUT() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'PUT' });
}

export async function PATCH() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'PATCH' });
}

export async function DELETE() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'DELETE' });
}

export async function HEAD() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'HEAD' });
}

export async function OPTIONS() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'OPTIONS' });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'GET' });
}

export async function POST() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'POST' });
}

export async function PUT() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'PUT' });
}

export async function PATCH() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'PATCH' });
}

export async function DELETE() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'DELETE' });
}

export async function HEAD() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'HEAD' });
}

export async function OPTIONS() {
return NextResponse.json({ data: 'I am a dynamic route!', method: 'OPTIONS' });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export async function GET() {
throw new Error('I am an error inside a dynamic route!');
}

export async function POST() {
throw new Error('I am an error inside a dynamic route!');
}

export async function PUT() {
throw new Error('I am an error inside a dynamic route!');
}

export async function PATCH() {
throw new Error('I am an error inside a dynamic route!');
}

export async function DELETE() {
throw new Error('I am an error inside a dynamic route!');
}

export async function HEAD() {
throw new Error('I am an error inside a dynamic route!');
}

export async function OPTIONS() {
throw new Error('I am an error inside a dynamic route!');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export async function GET() {
throw new Error('I am an error inside a dynamic route!');
}

export async function POST() {
throw new Error('I am an error inside a dynamic route!');
}

export async function PUT() {
throw new Error('I am an error inside a dynamic route!');
}

export async function PATCH() {
throw new Error('I am an error inside a dynamic route!');
}

export async function DELETE() {
throw new Error('I am an error inside a dynamic route!');
}

export async function HEAD() {
throw new Error('I am an error inside a dynamic route!');
}

export async function OPTIONS() {
throw new Error('I am an error inside a dynamic route!');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export async function GET() {
throw new Error('I am an error inside an edge route!');
}

export const runtime = 'experimental-edge';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json({ data: 'I am an edge route!', method: 'GET' });
}

export const runtime = 'experimental-edge';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { NextResponse } from 'next/server';

export async function GET() {
return NextResponse.json({ data: 'I am a static route!', method: 'GET' });
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as assert from 'assert/strict';

const stdin = fs.readFileSync(0).toString();

// Assert that all static components stay static and ally dynamic components stay dynamic
// Assert that all static components stay static and all dynamic components stay dynamic

assert.match(stdin, /○ \/client-component/);
assert.match(stdin, /● \/client-component\/parameter\/\[\.\.\.parameters\]/);
Expand All @@ -13,4 +13,10 @@ assert.match(stdin, /λ \/server-component/);
assert.match(stdin, /λ \/server-component\/parameter\/\[\.\.\.parameters\]/);
assert.match(stdin, /λ \/server-component\/parameter\/\[parameter\]/);

export {};
// Assert that all static route hndlers stay static and all dynamic route handlers stay dynamic

assert.match(stdin, /λ \/dynamic-route\/\[\.\.\.parameters\]/);
assert.match(stdin, /λ \/dynamic-route\/\[parameter\]/);
assert.match(stdin, /λ \/dynamic-route\/error\/\[\.\.\.parameters\]/);
assert.match(stdin, /λ \/dynamic-route\/error\/\[parameter\]/);
// assert.match(stdin, /● \/static-route/);
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { captureException } from '@sentry/nextjs';
export function ClientErrorDebugTools() {
const transactionContextValue = useContext(TransactionContext);
const [transactionName, setTransactionName] = useState<string>('');
const [getRequestTarget, setGetRequestTarget] = useState<string>('');
const [postRequestTarget, setPostRequestTarget] = useState<string>('');

const [isFetchingAPIRoute, setIsFetchingAPIRoute] = useState<boolean>();
const [isFetchingEdgeAPIRoute, setIsFetchingEdgeAPIRoute] = useState<boolean>();
const [isFetchingExternalAPIRoute, setIsFetchingExternalAPIRoute] = useState<boolean>();
const [isSendeingGetRequest, setIsSendingGetRequest] = useState<boolean>();
const [isSendeingPostRequest, setIsSendingPostRequest] = useState<boolean>();
const [renderError, setRenderError] = useState<boolean>();

if (renderError) {
Expand Down Expand Up @@ -119,6 +123,51 @@ export function ClientErrorDebugTools() {
Send request to external API route
</button>
<br />
<input
type="text"
placeholder="GET request target"
value={getRequestTarget}
onChange={e => {
setGetRequestTarget(e.target.value);
}}
/>
<button
onClick={async () => {
setIsSendingGetRequest(true);
try {
await fetch(getRequestTarget);
} catch (e) {
captureException(e);
}
setIsSendingGetRequest(false);
}}
>
Send GET request
</button>
<br />
<input
type="text"
placeholder="POST request target"
value={postRequestTarget}
onChange={e => {
setPostRequestTarget(e.target.value);
}}
/>
<button
onClick={async () => {
setIsSendingPostRequest(true);
try {
await fetch(postRequestTarget, {
method: 'POST',
});
} catch (e) {
captureException(e);
}
setIsSendingPostRequest(false);
}}
>
Send POST request
</button>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ if (!testEnv) {
const config: PlaywrightTestConfig = {
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
Expand All @@ -26,7 +26,8 @@ const config: PlaywrightTestConfig = {
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* `next dev` is incredibly buggy with the app dir */
retries: testEnv === 'development' ? 3 : 0,
/* `next build && next start` is also flakey. Current assumption: Next.js has a bug - but not sure. */
retries: 3,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ test.describe('dev mode error symbolification', () => {
function: 'onClick',
filename: 'components/client-error-debug-tools.tsx',
abs_path: 'webpack-internal:///(app-client)/./components/client-error-debug-tools.tsx',
lineno: 54,
lineno: 58,
colno: 16,
in_app: true,
pre_context: [' <button', ' onClick={() => {'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { test, expect } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { waitForError } from '../../../test-utils/event-proxy-server';
import axios, { AxiosError } from 'axios';
import { pollEventOnSentry } from './utils';

const authToken = process.env.E2E_TEST_AUTH_TOKEN;
const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG;
const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT;
const EVENT_POLLING_TIMEOUT = 30_000;

test('Sends a client-side exception to Sentry', async ({ page }) => {
test('Sends an ingestable client-side exception to Sentry', async ({ page }) => {
await page.goto('/');

const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
Expand All @@ -19,31 +14,37 @@ test('Sends a client-side exception to Sentry', async ({ page }) => {
const errorEvent = await errorEventPromise;
const exceptionEventId = errorEvent.event_id;

await expect
.poll(
async () => {
try {
const response = await axios.get(
`https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`,
{ headers: { Authorization: `Bearer ${authToken}` } },
);

return response.status;
} catch (e) {
if (e instanceof AxiosError && e.response) {
if (e.response.status !== 404) {
throw e;
} else {
return e.response.status;
}
} else {
throw e;
}
}
},
{
timeout: EVENT_POLLING_TIMEOUT,
},
)
.toBe(200);
expect(exceptionEventId).toBeDefined();
await pollEventOnSentry(exceptionEventId!);
});

// TODO: Fix that these tests are flakey on dev server - might be an SDK bug - might be Next.js itself
if (process.env.TEST_ENV !== 'development') {
test('Sends an ingestable route handler exception to Sentry', async ({ page }) => {
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'I am an error inside a dynamic route!';
});

await page.request.get('/dynamic-route/error/42');

const errorEvent = await errorEventPromise;
const exceptionEventId = errorEvent.event_id;

expect(exceptionEventId).toBeDefined();
await pollEventOnSentry(exceptionEventId!);
});

test('Sends an ingestable edge route handler exception to Sentry', async ({ page }) => {
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
return errorEvent?.exception?.values?.[0]?.value === 'I am an error inside an edge route!';
});

await page.request.get('/edge-route/error');

const errorEvent = await errorEventPromise;
const exceptionEventId = errorEvent.event_id;

expect(exceptionEventId).toBeDefined();
await pollEventOnSentry(exceptionEventId!);
});
}
Loading