Skip to content

Commit 35713ee

Browse files
committed
page transactions working, API transactions not working
1 parent 89c1b87 commit 35713ee

File tree

5 files changed

+130
-7
lines changed

5 files changed

+130
-7
lines changed

packages/nextjs/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"tslib": "^1.9.3"
2727
},
2828
"devDependencies": {
29+
"@sentry/tracing": "6.3.6",
2930
"@sentry/types": "6.3.6",
3031
"@types/webpack": "^5.28.0",
3132
"eslint": "7.20.0",

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { deepReadDirSync } from '@sentry/node';
2+
import { hasTracingEnabled } from '@sentry/tracing';
3+
import { Transaction } from '@sentry/types';
14
import { fill } from '@sentry/utils';
25
import * as http from 'http';
36
import { default as createNextServer } from 'next';
7+
import * as path from 'path';
48
import * as url from 'url';
59

610
import * as Sentry from '../index.server';
@@ -10,23 +14,33 @@ type PlainObject<T = any> = { [key: string]: T };
1014

1115
interface NextServer {
1216
server: Server;
17+
createServer: (options: PlainObject) => Server;
1318
}
1419

1520
interface Server {
1621
dir: string;
22+
publicDir: string;
23+
}
24+
25+
interface NextRequest extends http.IncomingMessage {
26+
cookies: Record<string, string>;
27+
url: string;
28+
}
29+
30+
interface NextResponse extends http.ServerResponse {
31+
__sentry__: {
32+
transaction?: Transaction;
33+
};
1734
}
1835

1936
type HandlerGetter = () => Promise<ReqHandler>;
20-
type ReqHandler = (
21-
req: http.IncomingMessage,
22-
res: http.ServerResponse,
23-
parsedUrl?: url.UrlWithParsedQuery,
24-
) => Promise<void>;
37+
type ReqHandler = (req: NextRequest, res: NextResponse, parsedUrl?: url.UrlWithParsedQuery) => Promise<void>;
2538
type ErrorLogger = (err: Error) => void;
2639

2740
// these aliases are purely to make the function signatures more easily understandable
2841
type WrappedHandlerGetter = HandlerGetter;
2942
type WrappedErrorLogger = ErrorLogger;
43+
type WrappedReqHandler = ReqHandler;
3044

3145
// TODO is it necessary for this to be an object?
3246
const closure: PlainObject = {};
@@ -61,12 +75,16 @@ function makeWrappedHandlerGetter(origHandlerGetter: HandlerGetter): WrappedHand
6175
const wrappedHandlerGetter = async function(this: NextServer): Promise<ReqHandler> {
6276
if (!closure.wrappingComplete) {
6377
closure.projectRootDir = this.server.dir;
78+
closure.server = this.server;
79+
closure.publicDir = this.server.publicDir;
6480

6581
const serverPrototype = Object.getPrototypeOf(this.server);
6682

6783
// wrap the logger so we can capture errors in page-level functions like `getServerSideProps`
6884
fill(serverPrototype, 'logError', makeWrappedErrorLogger);
6985

86+
fill(serverPrototype, 'handleRequest', makeWrappedReqHandler);
87+
7088
closure.wrappingComplete = true;
7189
}
7290

@@ -89,3 +107,77 @@ function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogge
89107
return origErrorLogger.call(this, err);
90108
};
91109
}
110+
111+
/**
112+
* Wrap the server's request handler to be able to create request transactions.
113+
*
114+
* @param origReqHandler The original request handler from the `Server` class
115+
* @returns A wrapped version of that handler
116+
*/
117+
function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
118+
const liveServer = closure.server as Server;
119+
120+
// inspired by
121+
// https://github.com/vercel/next.js/blob/4443d6f3d36b107e833376c2720c1e206eee720d/packages/next/next-server/server/next-server.ts#L1166
122+
const publicDirFiles = new Set(
123+
deepReadDirSync(liveServer.publicDir).map(p =>
124+
encodeURI(
125+
// switch any backslashes in the path to regular slashes
126+
p.replace(/\\/g, '/'),
127+
),
128+
),
129+
);
130+
131+
// add transaction start and stop to the normal request handling
132+
const wrappedReqHandler = async function(
133+
this: Server,
134+
req: NextRequest,
135+
res: NextResponse,
136+
parsedUrl?: url.UrlWithParsedQuery,
137+
): Promise<void> {
138+
// We only want to record page and API requests
139+
if (hasTracingEnabled() && shouldTraceRequest(req.url, publicDirFiles)) {
140+
const transaction = Sentry.startTransaction({
141+
name: `${(req.method || 'GET').toUpperCase()} ${req.url}`,
142+
op: 'http.server',
143+
});
144+
Sentry.getCurrentHub()
145+
.getScope()
146+
?.setSpan(transaction);
147+
148+
res.__sentry__ = {};
149+
res.__sentry__.transaction = transaction;
150+
}
151+
152+
res.once('finish', () => {
153+
const transaction = res.__sentry__?.transaction;
154+
if (transaction) {
155+
// Push `transaction.finish` to the next event loop so open spans have a chance to finish before the transaction
156+
// closes
157+
setImmediate(() => {
158+
// TODO
159+
// addExpressReqToTransaction(transaction, req);
160+
transaction.setHttpStatus(res.statusCode);
161+
transaction.finish();
162+
});
163+
}
164+
});
165+
166+
return origReqHandler.call(this, req, res, parsedUrl);
167+
};
168+
169+
return wrappedReqHandler;
170+
}
171+
172+
/**
173+
* Determine if the request should be traced, by filtering out requests for internal next files and static resources.
174+
*
175+
* @param url The URL of the request
176+
* @param publicDirFiles A set containing relative paths to all available static resources (note that this does not
177+
* include static *pages*, but rather images and the like)
178+
* @returns false if the URL is for an internal or static resource
179+
*/
180+
function shouldTraceRequest(url: string, publicDirFiles: Set<string>): boolean {
181+
// `static` is a deprecated but still-functional location for static resources
182+
return !url.startsWith('/_next/') && !url.startsWith('/static/') && !publicDirFiles.has(url);
183+
}

packages/node/src/handlers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,11 @@ export function extractRequestData(
255255
if (method === 'GET' || method === 'HEAD') {
256256
break;
257257
}
258-
// body data:
259-
// node, express, koa: req.body
258+
// body data: express, koa: req.body
259+
260+
// when using node by itself, you have to read the incoming stream(see
261+
// https://nodejs.dev/learn/get-http-request-body-data-using-nodejs); if a user is doing that, we can't know
262+
// where they're going to store the final result, so they'll have to capture this data themselves
260263
if (req.body !== undefined) {
261264
requestData.data = isString(req.body) ? req.body : JSON.stringify(normalize(req.body));
262265
}

packages/node/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export {
4141
export { NodeBackend, NodeOptions } from './backend';
4242
export { NodeClient } from './client';
4343
export { defaultIntegrations, init, lastEventId, flush, close, getSentryRelease } from './sdk';
44+
export { deepReadDirSync } from './utils';
4445
export { SDK_NAME } from './version';
4546

4647
import { Integrations as CoreIntegrations } from '@sentry/core';

packages/node/src/utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
4+
/**
5+
* Recursively read the contents of a directory.
6+
*
7+
* @param targetDir The directory to scan. All returned paths will be relative to this directory.
8+
* @param _paths Array to hold results, passed for purposes of recursion. Not meant to be provided by the caller.
9+
* @returns Array holding all relative paths
10+
*/
11+
export function deepReadDirSync(targetDir: string, _paths?: string[]): string[] {
12+
const paths = _paths || [];
13+
const currentDirContents = fs.readdirSync(targetDir);
14+
15+
currentDirContents.forEach((fileOrDirName: string) => {
16+
const fileOrDirAbsPath = path.join(targetDir, fileOrDirName);
17+
18+
if (fs.statSync(fileOrDirAbsPath).isDirectory()) {
19+
deepReadDirSync(fileOrDirAbsPath, paths);
20+
return;
21+
}
22+
paths.push(fileOrDirAbsPath.replace(targetDir, ''));
23+
});
24+
25+
return paths;
26+
}

0 commit comments

Comments
 (0)