-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(errors): Add support for handling chained exceptions #24572
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
Changes from all commits
903b039
3706e87
17b6bde
0b99859
7262d83
1132160
ee215de
071e836
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,16 @@ interface StackFrame { | |
| lineno: number | ||
| colno: number | ||
| function: string | ||
| context_line?: string | ||
| } | ||
|
|
||
| interface ExceptionTrace { | ||
| stacktrace: { | ||
| frames: StackFrame[] | ||
| } | ||
| module: string | ||
| type: string | ||
| value: string | ||
| } | ||
|
|
||
| function parseToFrames(rawTrace: string): StackFrame[] { | ||
|
|
@@ -25,7 +35,7 @@ function StackTrace({ rawTrace }: { rawTrace: string }): JSX.Element | null { | |
| <> | ||
| {frames.length ? ( | ||
| frames.map((frame, index) => { | ||
| const { filename, lineno, colno, function: functionName } = frame | ||
| const { filename, lineno, colno, function: functionName, context_line } = frame | ||
|
|
||
| return ( | ||
| <TitledSnack | ||
|
|
@@ -34,6 +44,7 @@ function StackTrace({ rawTrace }: { rawTrace: string }): JSX.Element | null { | |
| value={ | ||
| <> | ||
| {filename}:{lineno}:{colno} | ||
| {context_line ? `:${context_line}` : ''} | ||
| </> | ||
| } | ||
| /> | ||
|
|
@@ -51,6 +62,23 @@ function StackTrace({ rawTrace }: { rawTrace: string }): JSX.Element | null { | |
| } | ||
| } | ||
|
|
||
| function ChainedStackTraces({ exceptionList }: { exceptionList: ExceptionTrace[] }): JSX.Element { | ||
| return ( | ||
| <> | ||
| <LemonDivider dashed={true} /> | ||
| <h2 className="mb-0">Stack Trace</h2> | ||
| {exceptionList.map(({ stacktrace, value }, index) => { | ||
| return ( | ||
| <div key={index} className="flex flex-col gap-1 mt-6"> | ||
| <h3 className="mb-0">{value}</h3> | ||
| <StackTrace rawTrace={JSON.stringify(stacktrace.frames)} /> | ||
| </div> | ||
| ) | ||
| })} | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| function ActiveFlags({ flags }: { flags: string[] }): JSX.Element { | ||
| return ( | ||
| <> | ||
|
|
@@ -91,6 +119,7 @@ export function getExceptionPropertiesFrom(eventProperties: Record<string, any>) | |
| } = eventProperties | ||
|
|
||
| let $exception_stack_trace_raw = eventProperties.$exception_stack_trace_raw | ||
| let $exception_list = eventProperties.$exception_list | ||
| // exception autocapture sets $exception_stack_trace_raw as a string | ||
| // if it isn't present then this is probably a sentry exception. | ||
| // try and grab the frames from that | ||
|
|
@@ -102,6 +131,14 @@ export function getExceptionPropertiesFrom(eventProperties: Record<string, any>) | |
| } | ||
| } | ||
| } | ||
| // exception autocapture sets $exception_list for chained exceptions. | ||
| // If it's not present, get this list from the sentry_exception | ||
| if (!$exception_list?.length && $sentry_exception) { | ||
| if (Array.isArray($sentry_exception.values)) { | ||
| $exception_list = $sentry_exception.values | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| $exception_type, | ||
| $exception_message, | ||
|
|
@@ -115,6 +152,7 @@ export function getExceptionPropertiesFrom(eventProperties: Record<string, any>) | |
| $active_feature_flags, | ||
| $sentry_url, | ||
| $exception_stack_trace_raw, | ||
| $exception_list, | ||
| $level, | ||
| } | ||
| } | ||
|
|
@@ -133,6 +171,7 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' | |
| $active_feature_flags, | ||
| $sentry_url, | ||
| $exception_stack_trace_raw, | ||
| $exception_list, | ||
| $level, | ||
| } = getExceptionPropertiesFrom(eventProperties) | ||
|
|
||
|
|
@@ -162,18 +201,20 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' | |
| /> | ||
| <TitledSnack title="synthetic" value={$exception_synthetic ? 'true' : 'false'} /> | ||
| <TitledSnack title="library" value={`${$lib} ${$lib_version}`} /> | ||
| <TitledSnack title="browser" value={`${$browser} ${$browser_version}`} /> | ||
| <TitledSnack title="os" value={`${$os} ${$os_version}`} /> | ||
| <TitledSnack title="browser" value={$browser ? `${$browser} ${$browser_version}` : 'unknown'} /> | ||
| <TitledSnack title="os" value={$os ? `${$os} ${$os_version}` : 'unknown'} /> | ||
| </div> | ||
| {!!$exception_stack_trace_raw?.length && ( | ||
| {$exception_list?.length ? ( | ||
| <ChainedStackTraces exceptionList={$exception_list} /> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sentry reverses the
I think the SDK has to capture already in the right order, and the parsing/showing has to maintain that order, I am not sure this is correct right now assuming that we don't do the reverse either.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In python we effectively reverse so the first exception that happens is at the top. Maintaining this invariant in the SDKs this should be ok - as long as every SDK of ours does the same?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think each platform has to decide whether should reverse or not (this varies depending on the platform). |
||
| ) : $exception_stack_trace_raw?.length ? ( | ||
| <> | ||
| <LemonDivider dashed={true} /> | ||
| <div className="flex flex-col gap-1 mt-6"> | ||
| <h2 className="mb-0">Stack Trace</h2> | ||
| <StackTrace rawTrace={$exception_stack_trace_raw} /> | ||
| </div> | ||
| </> | ||
| )} | ||
| ) : null} | ||
| <LemonDivider dashed={true} /> | ||
| <div className="flex flex-col gap-1 mt-6"> | ||
| <h2 className="mb-0">Active Feature Flags</h2> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,27 @@ describe('Error Display', () => { | |
| $exception_message: 'There was an error creating the support ticket with zendesk.', | ||
| $exception_stack_trace_raw: | ||
| '[{"colno":220,"filename":"https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js","function":"submitZendeskTicket","in_app":true,"lineno":25}]', | ||
| $exception_list: [ | ||
| { | ||
| mechanism: { | ||
| handled: true, | ||
| type: 'generic', | ||
| }, | ||
| stacktrace: { | ||
| frames: [ | ||
| { | ||
| colno: 220, | ||
| filename: 'https://app-static-prod.posthog.com/static/chunk-UFQKIDIH.js', | ||
| function: 'submitZendeskTicket', | ||
| in_app: true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are not using this right now but its definitely important once we figure out grouping and stack trace demangling |
||
| lineno: 25, | ||
| }, | ||
| ], | ||
| }, | ||
| type: 'Error', | ||
| value: 'There was an error creating the support ticket with zendesk.', | ||
| }, | ||
| ], | ||
| $exception_synthetic: undefined, | ||
| $exception_type: 'Error', | ||
| $lib: 'posthog-js', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels like
sentry_exceptionshould have been just a$exception_listwith a single item, so we don't need to do this (that's how sentry does btw).https://develop.sentry.dev/sdk/event-payloads/types/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I get this, I think we need to do this because
$exception_listis a list of exceptions while sentry has a dict with the key 'values' which then has a list of exceptions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so about the first comment.
sentry does either:
{ "exception": {} }or
{ "exception": { "values": [] } }this is technical debt since chained exceptions weren't a thing before or not implemented at least.
backend|frontend has to check if it's either a single exception or a list of exceptions based on the
valuesbeing present or not.Always being:
{ "exception": { "values": [its ok to be a single item here even if its not a chained exception] } }will reduce those unnecessary checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in our case, we'd always use
$exception_list, even if it is a single item, and forget aboutsentry_exceptionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah correct, sounds like we're aligned. I have this here for the case where we're extracting and displaying the exception from sentry, not one captured via posthog (posthog will always send a list)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably still need this for backward compatibility but should we look to update the sentry integration to use
$exception_listover$sentry_exception? Happy for that to come in another PR if you preferThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quite like keeping these two separate, since that ensures we don't have a hard dependency on sentry's schema - the sentry integration can always just get the sentry exception, in sentry's format, and then there's this translation layer that converts it to ours where need be. Doing it here means there's only one place we need to do the conversion (rather than in all SDKs that eventually might support this), and also we don't need to rush to update SDKs when sentry adds/removes things from their schema
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unless we are using a super old version of the sentry js SDK, the
event.exceptionis always an object that contains avalueslist, so technically this workaround would not even be neededhttps://github.com/getsentry/sentry-javascript/blob/8ce9f7ce9a122bfe70d3c12ebfe50bba5d237037/packages/utils/src/eventbuilder.ts#L150-L154
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, the point is if our
$exception_listdiverges from sentry (like for example, the mechanism is set differently, handled is not a boolean), then we can do that translation from a sentry exception to a posthog exception here. But if we don't know that it came from a sentry exception, this becomes much more annoying, since you then need to test if mechanism is one of acceptable values, and if not, convert it, etc. etc.This is just a translation layer from sentry exception to posthog exception, not a workaround, if I'm understanding this code correctly, so I'd always want this because of reasons in above comment 😅