Skip to content

Commit 472cb0c

Browse files
authored
ref(core): Renormalize event only after stringification errors (#4425)
Since at least 5.19.1, we've had a bug[1] which causes events not to be able to be serialized for transmission, because they contain circular references. In theory, this should never happen, because all events go through a normalization process which [removes circular references](https://github.com/getsentry/sentry-javascript/blob/6abb46374763a49b60b883e1190d5dfda8eda842/packages/utils/src/object.ts#L338-L341). Nonetheless, the problem persists. There are three possibilities: 1) The normalization function is somehow not getting called for certain events. 2) Data containing a circular reference is being added to the event between normalization and serialization. 3) The normalization function doesn't catch (and fix) all cases in which circular references exist. In #3776, a debug flag was added which causes normalization to run twice, as a way to protect against possibility 1. As described in the above-linked issue, though, this hasn't solved the problem, at least not completely, as the serialization error is still occurring for some people. This PR aims to improve on that initial debugging step, by making the following changes: - The initial attempt at serialization is wrapped in a try-catch, so that any errors that arise can be collected _alongside_ the real event (as a `JSONStringifyError` tag and as a stacktrace in `event.extra`), rather than instead of it. - Events which go through the initial normalization are tagged internally, so that if the serializer encounters an event which _doesn't_ have the tag, it can note that (as a `skippedNormalization` tag on the event). In theory this should never show up, but if it does, it points to possibility 1. - Renormalization has been moved so that it's part of the serialization process itself. If this fixes the problem, that points to possibility 2. - Renormalization has been wrapped in a try-catch, with a `JSON.stringify error after renormalization` event logged to Sentry (again with the error in `extra` data) in cases where it fails. This is another situation which should never happen, but if it does, it points to possibility 3. Also, this is not specifically for debugging, but a bonus side effect of moving the renormalization to be part of serialization is that it allows us to only renormalize if theres's a problem, which eliminates a relatively computationally expensive operation in cases where it's not needed and therefore lets us ditch the debug flag. P.S. Disclaimer: I know this isn't all that pretty, but my assumption is that this will stay in the SDK for a release or two while we (hopefully) finally solve the mystery, and then be pulled back out before we ship v7. [1] #2809
1 parent c224f9c commit 472cb0c

File tree

3 files changed

+68
-7
lines changed

3 files changed

+68
-7
lines changed

packages/core/src/baseclient.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,7 @@ export abstract class BaseClient<B extends Backend, O extends Options> implement
428428
normalized.contexts.trace = event.contexts.trace;
429429
}
430430

431-
const { _experiments = {} } = this.getOptions();
432-
if (_experiments.ensureNoCircularStructures) {
433-
return normalize(normalized);
434-
}
431+
event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, baseClientNormalized: true };
435432

436433
return normalized;
437434
}

packages/core/src/request.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Event, SdkInfo, SentryRequest, SentryRequestType, Session, SessionAggregates } from '@sentry/types';
2-
import { dsnToString } from '@sentry/utils';
2+
import { dsnToString, normalize } from '@sentry/utils';
33

44
import { APIDetails, getEnvelopeEndpointWithUrlEncodedAuth, getStoreEndpointWithUrlEncodedAuth } from './api';
55

@@ -58,11 +58,58 @@ export function eventToSentryRequest(event: Event, api: APIDetails): SentryReque
5858
const { transactionSampling } = event.sdkProcessingMetadata || {};
5959
const { method: samplingMethod, rate: sampleRate } = transactionSampling || {};
6060

61+
// TODO: Below is a temporary hack in order to debug a serialization error - see
62+
// https://github.com/getsentry/sentry-javascript/issues/2809 and
63+
// https://github.com/getsentry/sentry-javascript/pull/4425. TL;DR: even though we normalize all events (which should
64+
// prevent this), something is causing `JSON.stringify` to throw a circular reference error.
65+
//
66+
// When it's time to remove it:
67+
// 1. Delete everything between here and where the request object `req` is created, EXCEPT the line deleting
68+
// `sdkProcessingMetadata`
69+
// 2. Restore the original version of the request body, which is commented out
70+
// 3. Search for `skippedNormalization` and pull out the companion hack in the browser playwright tests
71+
enhanceEventWithSdkInfo(event, api.metadata.sdk);
72+
event.tags = event.tags || {};
73+
event.extra = event.extra || {};
74+
75+
// In theory, all events should be marked as having gone through normalization and so
76+
// we should never set this tag
77+
if (!(event.sdkProcessingMetadata && event.sdkProcessingMetadata.baseClientNormalized)) {
78+
event.tags.skippedNormalization = true;
79+
}
80+
6181
// prevent this data from being sent to sentry
82+
// TODO: This is NOT part of the hack - DO NOT DELETE
6283
delete event.sdkProcessingMetadata;
6384

85+
let body;
86+
try {
87+
// 99.9% of events should get through just fine - no change in behavior for them
88+
body = JSON.stringify(event);
89+
} catch (err) {
90+
// Record data about the error without replacing original event data, then force renormalization
91+
event.tags.JSONStringifyError = true;
92+
event.extra.JSONStringifyError = err;
93+
try {
94+
body = JSON.stringify(normalize(event));
95+
} catch (newErr) {
96+
// At this point even renormalization hasn't worked, meaning something about the event data has gone very wrong.
97+
// Time to cut our losses and record only the new error. With luck, even in the problematic cases we're trying to
98+
// debug with this hack, we won't ever land here.
99+
const innerErr = newErr as Error;
100+
body = JSON.stringify({
101+
message: 'JSON.stringify error after renormalization',
102+
// setting `extra: { innerErr }` here for some reason results in an empty object, so unpack manually
103+
extra: { message: innerErr.message, stack: innerErr.stack },
104+
});
105+
}
106+
}
107+
64108
const req: SentryRequest = {
65-
body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event),
109+
// this is the relevant line of code before the hack was added, to make it easy to undo said hack once we've solved
110+
// the mystery
111+
// body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event),
112+
body,
66113
type: eventType,
67114
url: useEnvelope
68115
? getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel)

packages/integration-tests/utils/helpers.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,24 @@ async function getMultipleRequests(
8787
if (urlRgx.test(request.url())) {
8888
try {
8989
reqCount -= 1;
90-
requestData.push(requestParser(request));
90+
91+
// TODO: This is to compensate for a temporary debugging hack which adds data the tests aren't anticipating to
92+
// the request. The code can be restored to its original form (the commented-out line below) once that hack is
93+
// removed. See https://github.com/getsentry/sentry-javascript/pull/4425.
94+
const parsedRequest = requestParser(request);
95+
if (parsedRequest.tags) {
96+
if (parsedRequest.tags.skippedNormalization && Object.keys(parsedRequest.tags).length === 1) {
97+
delete parsedRequest.tags;
98+
} else {
99+
delete parsedRequest.tags.skippedNormalization;
100+
}
101+
}
102+
if (parsedRequest.extra && Object.keys(parsedRequest.extra).length === 0) {
103+
delete parsedRequest.extra;
104+
}
105+
requestData.push(parsedRequest);
106+
// requestData.push(requestParser(request));
107+
91108
if (reqCount === 0) {
92109
resolve(requestData);
93110
}

0 commit comments

Comments
 (0)