Skip to content
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

ref(nextjs): UseRequestData integration for errors #5729

Merged
14 changes: 1 addition & 13 deletions packages/nextjs/src/config/wrappers/wrapperUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { captureException, getCurrentHub, startTransaction } from '@sentry/core';
import { addRequestDataToEvent } from '@sentry/node';
import { getActiveTransaction } from '@sentry/tracing';
import { Transaction } from '@sentry/types';
import { baggageHeaderToDynamicSamplingContext, extractTraceparentData, fill } from '@sentry/utils';
Expand Down Expand Up @@ -123,18 +122,7 @@ export function callTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
const currentScope = getCurrentHub().getScope();
if (currentScope) {
currentScope.setSpan(dataFetcherSpan);
currentScope.addEventProcessor(event =>
event.type !== 'transaction'
? addRequestDataToEvent(event, req, {
include: {
// When the `transaction` option is set to true, it tries to extract a transaction name from the request
// object. We don't want this since we already have a high-quality transaction name with a parameterized
// route. Setting `transaction` to `true` will clobber that transaction name.
transaction: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just took me 20 minutes to figure out why it is safe to remove the transaction: false logic here. I believe what confused me since we don't have the include.transaction defaulted to false in the requestdata integration. I think we should add that default just so it's more explicit.

Also (for a lack of better place to comment) there are still some TODO comments in addRequestDataToEvent in requestdata.ts referencing Next.js. Can we remove those now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It just took me 20 minutes to figure out why it is safe to remove the transaction: false logic here. I believe what confused me since we don't have the include.transaction defaulted to false in the requestdata integration. I think we should add that default just so it's more explicit.

include.transaction isn't a thing anymore, at least not for the integration. From the description of #5703:

  • The options API for the new integration is inspired by, but different from, the options API for our Express request handler.
    • In the handler options, transaction can either be a boolean or a TransactionNamingScheme. In the integration, it can no longer be a boolean - events are going to have transactions, one way or another, so we shouldn't let people set it to false. Further, since it's now not about whether transaction is or isn't included, it's been moved out of include into its own transactionNamingScheme option.

Regardless, we didn't actually need that setting in the first place, at least not for transaction events. Even if include.transaction is set to true, transaction events will never fall into the if because they always already have a transaction value:

if (include.transaction && !event.transaction) {

So that handles transaction events. As for error events, as of this PR it doesn't matter what transaction value addRequestDataToTransaction sets, because we're overwriting it in the integration.

(And sorry - I don't mean that to sound dismissive. It's a valid concern, just not one that happens to apply here.)

Also (for a lack of better place to comment) there are still some TODO comments in addRequestDataToEvent in requestdata.ts referencing Next.js. Can we remove those now?

Probably. I'll give them a look.

Copy link
Member

@lforst lforst Sep 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. I now remember why I set transaction: false in the first place - it was because the errors originating from data fetchers had the unparameterized route in a tag - which now seems to be the case again and I'm not sure this is a good thing, since the transaction tag now mismatches the actual transaction (Do we even need to be exact here though? I guess it might be confusing to users):

Example - here the transaction tag is GET /parameterized-page-with-getServerSideProps/two while it should be /parameterized-page-with-getServerSideProps/[pageNum]

Screen Shot 2022-09-20 at 10 43 13

Screen Shot 2022-09-20 at 10 47 40

(event (after), event (before))

I guess both are sort of correct, I am just wondering which one is better. Do you have an opinion here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There also seem to be events where the tag is the request url. Not sure if this is related to this change though:

event

Screen Shot 2022-09-20 at 10 56 24

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. In my testing, the tag was always overwritten with the transaction value, even when I specifically set it to be something different in beforeSend. The "after" event is from this branch?

(Also, there's no reason we can't just fix the tag, too, just to be certain.)

UPDATE: In your event(after), event(before) links, one thing I notice is that the before event doesn't have a transaction value at all (only a tag). Lemme play around with this a little more.

UPDATE2: Damn. Rebase fail. 🤦🏻‍♀️ An old bug crept back in - wrong naming scheme for the link to the transaction on the request. Now I want to go back and make sure nothing else regressed...

On the transaction score, I think we should be good now, though:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah before is some event before this PR and after is after checking out the PR.

Are we sure we want to have the GET in the transaction tag though? Since there will be a mismatch between actual transaction and transaction tag.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, discovered that as well. I think I managed to drop a fixup commit somewhere in there, which both fixed how the transaction is retrieved and the check for whether or not to include the method for nextjs events.

For nextjs, to make it match current behavior, the answer is yes for API routes, no for page routes. I'm proposing that in v8 we should just standardize on always including the method. (We already include it in our express transaction names, for example.)

there will be a mismatch between actual transaction and transaction tag.

From my testing, I believe the only way this can happen (in the UI, at least) is for an event to come in with a transaction tag but no top-level transaction value (like your "before" event). If an outgoing event has both a top-level transaction value and a transaction tag, and they don't match, ingest seems to overwrite the tag value with the top-level value. For example, this beforeSend

beforeSend: (event) => {
  event.transaction = "aardvark";
  event.tags.transaction = "zebra";
  return event;
}

produced an event with this JSON:

image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gut feeling wise I like the current behavior (yes for API routes, no for page routes), because that way the pageload/pagenavigation transaction names and data-fetcher transactions line up. But let's discuss this in-depth when we get to it!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not wedded to either outcome, I just think we should standardize it across SDKs if possible. The tricky part when it comes to something like Express is that it's not as clear-cut what's an API request and what's a page load request. But as you say, we can talk about it closer to the time.

},
})
: event,
);
currentScope.setSDKProcessingMetadata({ request: req });
}

try {
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/utils/_error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { captureException, withScope } from '@sentry/core';
import { getCurrentHub } from '@sentry/hub';
import { addExceptionMechanism, addRequestDataToEvent } from '@sentry/utils';
import { addExceptionMechanism } from '@sentry/utils';
import { NextPageContext } from 'next';

type ContextOrProps = {
Expand Down Expand Up @@ -55,7 +55,7 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP
});

if (req) {
scope.addEventProcessor(event => addRequestDataToEvent(event, req));
scope.setSDKProcessingMetadata({ request: req });
}

// If third-party libraries (or users themselves) throw something falsy, we want to capture it as a message (which
Expand Down
18 changes: 3 additions & 15 deletions packages/nextjs/src/utils/instrumentServer.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
/* eslint-disable max-lines */
import {
addRequestDataToEvent,
captureException,
configureScope,
deepReadDirSync,
getCurrentHub,
startTransaction,
} from '@sentry/node';
import { captureException, configureScope, deepReadDirSync, getCurrentHub, startTransaction } from '@sentry/node';
import { extractTraceparentData, getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
import {
addExceptionMechanism,
Expand Down Expand Up @@ -244,9 +237,8 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
const currentScope = getCurrentHub().getScope();

if (currentScope) {
currentScope.addEventProcessor(event =>
event.type !== 'transaction' ? addRequestDataToEvent(event, nextReq) : event,
);
// Store the request on the scope so we can pull data from it and add it to the event
currentScope.setSDKProcessingMetadata({ request: req });

// We only want to record page and API requests
if (hasTracingEnabled() && shouldTraceRequest(nextReq.url, publicDirFiles)) {
Expand Down Expand Up @@ -297,10 +289,6 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
if (transaction) {
transaction.setHttpStatus(res.statusCode);

// we'll collect this data in a more targeted way in the event processor we added above,
// `addRequestDataToEvent`
delete transaction.metadata.requestPath;

// Push `transaction.finish` to the next event loop so open spans have a chance to finish before the
// transaction closes
setImmediate(() => {
Expand Down
6 changes: 2 additions & 4 deletions packages/nextjs/src/utils/withSentry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { addRequestDataToEvent, captureException, flush, getCurrentHub, startTransaction } from '@sentry/node';
import { captureException, flush, getCurrentHub, startTransaction } from '@sentry/node';
import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
import { Transaction } from '@sentry/types';
import {
Expand Down Expand Up @@ -56,9 +56,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
const currentScope = getCurrentHub().getScope();

if (currentScope) {
currentScope.addEventProcessor(event =>
event.type !== 'transaction' ? addRequestDataToEvent(event, req) : event,
);
currentScope.setSDKProcessingMetadata({ request: req });

if (hasTracingEnabled()) {
// If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
Expand Down
55 changes: 51 additions & 4 deletions packages/node/src/integrations/requestdata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// TODO (v8 or v9): Whenever this becomes a default integration for `@sentry/browser`, move this to `@sentry/core`. For
// now, we leave it in `@sentry/integrations` so that it doesn't contribute bytes to our CDN bundles.

import { EventProcessor, Hub, Integration } from '@sentry/types';
import { EventProcessor, Hub, Integration, Transaction } from '@sentry/types';
import { extractPathForTransaction } from '@sentry/utils';

import {
addRequestDataToEvent,
Expand Down Expand Up @@ -86,10 +87,15 @@ export class RequestData implements Integration {
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, getCurrentHub: () => Hub): void {
const { include, addRequestData } = this._options;
// Note: In the long run, most of the logic here should probably move into the request data utility functions. For
// the moment it lives here, though, until https://github.com/getsentry/sentry-javascript/issues/5718 is addressed.
// (TL;DR: Those functions touch many parts of the repo in many different ways, and need to be clened up. Once
// that's happened, it will be easier to add this logic in without worrying about unexpected side effects.)
const { include, addRequestData, transactionNamingScheme } = this._options;

addGlobalEventProcessor(event => {
const self = getCurrentHub().getIntegration(RequestData);
const hub = getCurrentHub();
const self = hub.getIntegration(RequestData);
const req = event.sdkProcessingMetadata && event.sdkProcessingMetadata.request;

// If the globally installed instance of this integration isn't associated with the current hub, `self` will be
Expand All @@ -98,7 +104,36 @@ export class RequestData implements Integration {
return event;
}

return addRequestData(event, req, { include: formatIncludeOption(include) });
const processedEvent = addRequestData(event, req, { include: formatIncludeOption(include) });

// Transaction events already have the right `transaction` value
if (event.type === 'transaction' || transactionNamingScheme === 'handler') {
return processedEvent;
}

// In all other cases, use the request's associated transaction (if any) to overwrite the event's `transaction`
// value with a high-quality one
const reqWithTransaction = req as { _sentryTransaction?: Transaction };
const transaction = reqWithTransaction._sentryTransaction;
if (transaction) {
// TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to
// keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential
// to break things like alert rules.)
const shouldIncludeMethodInTransactionName =
getSDKName(hub) === 'sentry.javascript.nextjs'
? transaction.name.startsWith('/api')
: transactionNamingScheme !== 'path';

const [transactionValue] = extractPathForTransaction(req, {
path: true,
method: shouldIncludeMethodInTransactionName,
customRoute: transaction.name,
});

processedEvent.transaction = transactionValue;
}

return processedEvent;
});
}
}
Expand All @@ -123,3 +158,15 @@ function formatIncludeOption(
request: requestIncludeKeys.length !== 0 ? requestIncludeKeys : undefined,
};
}

function getSDKName(hub: Hub): string | undefined {
try {
// For a long chain like this, it's fewer bytes to combine a try-catch with assuming everything is there than to
// write out a long chain of `a && a.b && a.b.c && ...`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hub.getClient()!.getOptions()!._metadata!.sdk!.name;
} catch (err) {
// In theory we should never get here
return undefined;
}
}