Skip to content

Commit 3fbe529

Browse files
authored
fix: next.js few fixes (#3479)
* fix: Use buildId as fallback release * fix: Make getRemainingTimeInMillis optional * fix: add serverless package as a dependecy * ref: Remove serverless package again * fix: Also apply to api pages * fix: Bind this for origErrorLogger * ref: Changelog
1 parent 3ab107b commit 3fbe529

File tree

5 files changed

+57
-38
lines changed

5 files changed

+57
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

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

7+
## 6.3.4
8+
9+
- [nextjs] fix: API routes logging (#3479)
10+
711
## 6.3.3
812

913
- [nextjs] fix: User server types (#3471)

packages/nextjs/src/utils/config.ts

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ type WebpackExport = (config: WebpackConfig, options: WebpackOptions) => Webpack
1515

1616
// The two arguments passed to the exported `webpack` function, as well as the thing it returns
1717
type WebpackConfig = { devtool: string; plugins: PlainObject[]; entry: EntryProperty };
18-
// TODO use real webpack types
19-
type WebpackOptions = { dev: boolean; isServer: boolean };
18+
type WebpackOptions = { dev: boolean; isServer: boolean; buildId: string };
2019

2120
// For our purposes, the value for `entry` is either an object, or a function which returns such an object
2221
type EntryProperty = (() => Promise<EntryPropertyObject>) | EntryPropertyObject;
@@ -27,33 +26,10 @@ type EntryProperty = (() => Promise<EntryPropertyObject>) | EntryPropertyObject;
2726
type EntryPropertyObject = PlainObject<string | Array<string> | EntryPointObject>;
2827
type EntryPointObject = { import: string | Array<string> };
2928

30-
const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryProperty> => {
31-
// Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
32-
// object has string arrays for values. But because we don't know whether someone else has come along before us and
33-
// changed that, we need to check a few things along the way.
34-
35-
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
36-
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
37-
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
38-
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
39-
// options. See https://webpack.js.org/configuration/entry-context/#entry.
40-
41-
let newEntryProperty = origEntryProperty;
42-
43-
if (typeof origEntryProperty === 'function') {
44-
newEntryProperty = await origEntryProperty();
45-
}
46-
47-
newEntryProperty = newEntryProperty as EntryPropertyObject;
48-
49-
// according to vercel, we only need to inject Sentry in one spot for server and one spot for client, and because
50-
// those are used as bases, it will apply everywhere
51-
const injectionPoint = isServer ? 'pages/_document' : 'main';
52-
const injectee = isServer ? './sentry.server.config.js' : './sentry.client.config.js';
53-
29+
/** Add a file (`injectee`) to a given element (`injectionPoint`) of the `entry` property */
30+
const _injectFile = (entryProperty: EntryPropertyObject, injectionPoint: string, injectee: string): void => {
5431
// can be a string, array of strings, or object whose `import` property is one of those two
55-
let injectedInto = newEntryProperty[injectionPoint];
56-
32+
let injectedInto = entryProperty[injectionPoint];
5733
// whatever the format, add in the sentry file
5834
injectedInto =
5935
typeof injectedInto === 'string'
@@ -73,15 +49,42 @@ const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean)
7349
: // array case for inner property
7450
[injectee, ...injectedInto.import],
7551
};
52+
entryProperty[injectionPoint] = injectedInto;
53+
};
7654

77-
newEntryProperty[injectionPoint] = injectedInto;
78-
55+
const injectSentry = async (origEntryProperty: EntryProperty, isServer: boolean): Promise<EntryProperty> => {
56+
// Out of the box, nextjs uses the `() => Promise<EntryPropertyObject>)` flavor of EntryProperty, where the returned
57+
// object has string arrays for values. But because we don't know whether someone else has come along before us and
58+
// changed that, we need to check a few things along the way.
59+
// The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs
60+
// sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether
61+
// someone else has come along before us and changed that, we need to check a few things along the way. The one thing
62+
// we know is that it won't have gotten *simpler* in form, so we only need to worry about the object and function
63+
// options. See https://webpack.js.org/configuration/entry-context/#entry.
64+
let newEntryProperty = origEntryProperty;
65+
if (typeof origEntryProperty === 'function') {
66+
newEntryProperty = await origEntryProperty();
67+
}
68+
newEntryProperty = newEntryProperty as EntryPropertyObject;
69+
// On the server, we need to inject the SDK into both into the base page (`_document`) and into individual API routes
70+
// (which have no common base).
71+
if (isServer) {
72+
Object.keys(newEntryProperty).forEach(key => {
73+
if (key === 'pages/_document' || key.includes('pages/api')) {
74+
// for some reason, because we're now in a function, we have to cast again
75+
_injectFile(newEntryProperty as EntryPropertyObject, key, './sentry.server.config.js');
76+
}
77+
});
78+
}
79+
// On the client, it's sufficient to inject it into the `main` JS code, which is included in every browser page.
80+
else {
81+
_injectFile(newEntryProperty, 'main', './sentry.client.config.js');
82+
}
7983
// TODO: hack made necessary because the async-ness of this function turns our object back into a promise, meaning the
8084
// internal `next` code which should do this doesn't
8185
if ('main.js' in newEntryProperty) {
8286
delete newEntryProperty['main.js'];
8387
}
84-
8588
return newEntryProperty;
8689
};
8790

@@ -97,7 +100,6 @@ export function withSentryConfig(
97100
providedWebpackPluginOptions: Partial<SentryCliPluginOptions> = {},
98101
): NextConfigExports {
99102
const defaultWebpackPluginOptions = {
100-
release: getSentryRelease(),
101103
url: process.env.SENTRY_URL,
102104
org: process.env.SENTRY_ORG,
103105
project: process.env.SENTRY_PROJECT,
@@ -144,6 +146,7 @@ export function withSentryConfig(
144146
// TODO it's not clear how to do this better, but there *must* be a better way
145147
new ((SentryWebpackPlugin as unknown) as typeof defaultWebpackPlugin)({
146148
dryRun: options.dev,
149+
release: getSentryRelease(options.buildId),
147150
...defaultWebpackPluginOptions,
148151
...providedWebpackPluginOptions,
149152
}),

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ function makeWrappedErrorLogger(origErrorLogger: ErrorLogger): WrappedErrorLogge
8686
return (err: Error): void => {
8787
// TODO add context data here
8888
Sentry.captureException(err);
89-
return origErrorLogger(err);
89+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
90+
// @ts-ignore
91+
return origErrorLogger.bind(this, err);
9092
};
9193
}

packages/node/src/sdk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export async function close(timeout?: number): Promise<boolean> {
151151
/**
152152
* Returns a release dynamically from environment variables.
153153
*/
154-
export function getSentryRelease(): string | undefined {
154+
export function getSentryRelease(fallback?: string): string | undefined {
155155
// Always read first as Sentry takes this as precedence
156156
if (process.env.SENTRY_RELEASE) {
157157
return process.env.SENTRY_RELEASE;
@@ -176,6 +176,7 @@ export function getSentryRelease(): string | undefined {
176176
// Zeit (now known as Vercel)
177177
process.env.ZEIT_GITHUB_COMMIT_SHA ||
178178
process.env.ZEIT_GITLAB_COMMIT_SHA ||
179-
process.env.ZEIT_BITBUCKET_COMMIT_SHA
179+
process.env.ZEIT_BITBUCKET_COMMIT_SHA ||
180+
fallback
180181
);
181182
}

packages/serverless/src/awslambda.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ export function tryPatchHandler(taskRoot: string, handlerPath: string): void {
131131
(mod as HandlerModule)[functionName!] = wrapHandler(obj as Handler);
132132
}
133133

134+
/**
135+
* Tries to invoke context.getRemainingTimeInMillis if not available returns 0
136+
* Some environments use AWS lambda but don't support this function
137+
* @param context
138+
*/
139+
function tryGetRemainingTimeInMillis(context: Context): number {
140+
return typeof context.getRemainingTimeInMillis === 'function' ? context.getRemainingTimeInMillis() : 0;
141+
}
142+
134143
/**
135144
* Adds additional information from the environment and AWS Context to the Sentry Scope.
136145
*
@@ -155,7 +164,7 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi
155164
function_version: context.functionVersion,
156165
invoked_function_arn: context.invokedFunctionArn,
157166
execution_duration_in_millis: performance.now() - startTime,
158-
remaining_time_in_millis: context.getRemainingTimeInMillis(),
167+
remaining_time_in_millis: tryGetRemainingTimeInMillis(context),
159168
'sys.argv': process.argv,
160169
});
161170

@@ -221,7 +230,7 @@ export function wrapHandler<TEvent, TResult>(
221230
context.callbackWaitsForEmptyEventLoop = options.callbackWaitsForEmptyEventLoop;
222231

223232
// In seconds. You cannot go any more granular than this in AWS Lambda.
224-
const configuredTimeout = Math.ceil(context.getRemainingTimeInMillis() / 1000);
233+
const configuredTimeout = Math.ceil(tryGetRemainingTimeInMillis(context) / 1000);
225234
const configuredTimeoutMinutes = Math.floor(configuredTimeout / 60);
226235
const configuredTimeoutSeconds = configuredTimeout % 60;
227236

@@ -233,7 +242,7 @@ export function wrapHandler<TEvent, TResult>(
233242
// When `callbackWaitsForEmptyEventLoop` is set to false, which it should when using `captureTimeoutWarning`,
234243
// we don't have a guarantee that this message will be delivered. Because of that, we don't flush it.
235244
if (options.captureTimeoutWarning) {
236-
const timeoutWarningDelay = context.getRemainingTimeInMillis() - options.timeoutWarningLimit;
245+
const timeoutWarningDelay = tryGetRemainingTimeInMillis(context) - options.timeoutWarningLimit;
237246

238247
timeoutWarningTimer = setTimeout(() => {
239248
withScope(scope => {

0 commit comments

Comments
 (0)