diff --git a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--dark.png b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--dark.png index 624848546ac3e..48731081d7d0b 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--dark.png and b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--light.png b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--light.png index 3204a87ef56f4..330fb5f078003 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--light.png and b/frontend/__snapshots__/components-errors-error-display--anonymous-error-with-stack-trace--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--chained-error-stack--dark.png b/frontend/__snapshots__/components-errors-error-display--chained-error-stack--dark.png index 87a9df0085cf6..9fc351cbabd77 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--chained-error-stack--dark.png and b/frontend/__snapshots__/components-errors-error-display--chained-error-stack--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--chained-error-stack--light.png b/frontend/__snapshots__/components-errors-error-display--chained-error-stack--light.png index 33d98017b94dd..9d370339b0ccf 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--chained-error-stack--light.png and b/frontend/__snapshots__/components-errors-error-display--chained-error-stack--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--dark.png b/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--dark.png new file mode 100644 index 0000000000000..c7d50fd42accb Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--light.png b/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--light.png new file mode 100644 index 0000000000000..4d6b2e0ba2357 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--legacy-event-properties--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--dark.png b/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--dark.png new file mode 100644 index 0000000000000..941cb33add551 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--light.png b/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--light.png new file mode 100644 index 0000000000000..18a440b4d6a61 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--sentry-stack-trace--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--dark.png b/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--dark.png index 457b11013108b..0df3e951f46ea 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--dark.png and b/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--light.png b/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--light.png index 3abb6c9b6327c..b9a4700958bb5 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--light.png and b/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--dark.png b/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--dark.png new file mode 100644 index 0000000000000..7b1cd735db425 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--light.png b/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--light.png new file mode 100644 index 0000000000000..110f5db83f6c9 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--stacktraceless-import-module-error--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--dark.png b/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--dark.png new file mode 100644 index 0000000000000..a7305b21d51cc Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--light.png b/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--light.png new file mode 100644 index 0000000000000..51d99aafc66e1 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--stacktraceless-safari-script-error--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--dark.png b/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--dark.png index fcbc4ceb12a2a..a1442f3a3296a 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--dark.png and b/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--light.png b/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--light.png index 40ff9d12b2ba2..fe861e856a587 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--light.png and b/frontend/__snapshots__/components-errors-error-display--with-cymbal-errors--light.png differ diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--error-event--dark.png b/frontend/__snapshots__/components-playerinspector-itemevent--error-event--dark.png index 2968bffa1b0af..cb9c980b9c9e1 100644 Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--error-event--dark.png and b/frontend/__snapshots__/components-playerinspector-itemevent--error-event--dark.png differ diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--error-event--light.png b/frontend/__snapshots__/components-playerinspector-itemevent--error-event--light.png index ffb3069d1ba44..53fcf0eccbc73 100644 Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--error-event--light.png and b/frontend/__snapshots__/components-playerinspector-itemevent--error-event--light.png differ diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--dark.png b/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--dark.png index 7241c43865cef..4387c307782f5 100644 Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--dark.png and b/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--dark.png differ diff --git a/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--light.png b/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--light.png index 3573a7ff1789f..35a00d4fda7bb 100644 Binary files a/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--light.png and b/frontend/__snapshots__/components-playerinspector-itemevent--sentry-error-event--light.png differ diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.scss b/frontend/src/lib/components/Errors/ErrorDisplay.scss deleted file mode 100644 index ec8c81c7ec1a3..0000000000000 --- a/frontend/src/lib/components/Errors/ErrorDisplay.scss +++ /dev/null @@ -1,11 +0,0 @@ -.ErrorDisplay__stacktrace { - .LemonCollapsePanel__header { - min-height: 1.875rem !important; - padding: 0 !important; - background-color: var(--accent-3000); - - &--disabled:hover { - background-color: var(--accent-3000) !important; - } - } -} diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx index c93643b3af17a..8033e0631487e 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx @@ -82,15 +82,13 @@ function errorProperties(properties: Record): EventType['properties customer: 'the-customer', instance: 'https://app.posthog.com', }, - $exception_message: 'ResizeObserver loop limit exceeded', - $exception_type: 'Error', $exception_fingerprint: 'Error', $exception_personURL: 'https://app.posthog.com/person/the-person-id', $sentry_event_id: 'id-from-the-sentry-integration', $sentry_exception: { values: [ { - value: 'ResizeObserver loop limit exceeded', + value: "DB::Exception: There was an error on [localhost:9000]: Code: 701. DB::Exception: Requested cluster 'posthog_single_shard' not found. (CLUSTER_DOESNT_EXIST) (version 23.11.2.11 (official build)). Stack trace: 0. DB::Exception::Exception(DB::Exception::MessageMasked&&, int, bool) @ 0x000000000c4fd597 in /usr/bin/clickhouse 1. DB::DDLQueryStatusSource::generate() @ 0x00000000113205f8 in /usr/bin/clickhouse 2. DB::ISource::tryGenerate() @ 0x0000000012290275 in /usr/bin/clickhouse 3. DB::ISource::work() @ 0x000000001228fcc3 in /usr/bin/clickhouse 4. DB::ExecutionThreadContext::executeTask() @ 0x00000000122a78ba in /usr/bin/clickhouse 5. DB::PipelineExecutor::executeStepImpl(unsigned long, std::atomic*) @ 0x000000001229e5d0 in /usr/bin/clickhouse 6. DB::PipelineExecutor::execute(unsigned long, bool) @ 0x000000001229d860 in /usr/bin/clickhouse 7. void std::__function::__policy_invoker::__call_impl::ThreadFromGlobalPoolImpl(DB::PullingAsyncPipelineExecutor::pull(DB::Chunk&, unsigned long)::$_0&&)::'lambda'(), void ()>>(std::__function::__policy_storage const*) @ 0x00000000122ab1cf in /usr/bin/clickhouse 8. void* std::__thread_proxy[abi:v15000]>, void ThreadPoolImpl::scheduleImpl(std::function, Priority, std::optional, bool)::'lambda0'()>>(void*) @ 0x000000000c5e45d3 in /usr/bin/clickhouse 9. ? @ 0x00007429a8071609 in ? 10. ? @ 0x00007429a7f96133 in ?", type: 'Error', mechanism: { type: 'onerror', @@ -135,42 +133,37 @@ function errorProperties(properties: Record): EventType['properties $lib_version__major: 1, $lib_version__minor: 63, $lib_version__patch: 3, + $exception_list: [ + { + value: 'ResizeObserver loop limit exceeded', + type: 'Error', + }, + ], ...properties, } } -export function ResizeObserverLoopLimitExceeded(): JSX.Element { - return ( - - ) -} - -export function SafariScriptError(): JSX.Element { +export function StacktracelessSafariScriptError(): JSX.Element { return ( ) } -export function ImportingModule(): JSX.Element { +export function StacktracelessImportModuleError(): JSX.Element { return ( ) @@ -207,8 +200,6 @@ export function ChainedErrorStack(): JSX.Element { return ( ) } -export function WithCymbalErrors(): JSX.Element { +export function SentryStackTrace(): JSX.Element { + return +} + +export function LegacyEventProperties(): JSX.Element { return ( ) diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.tsx index cc6899794e94f..98e5292da2346 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.tsx @@ -1,257 +1,36 @@ -import './ErrorDisplay.scss' - -import { LemonBanner, LemonCollapse, Tooltip } from '@posthog/lemon-ui' +import { LemonBanner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { TitledSnack } from 'lib/components/TitledSnack' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch' -import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' import { Link } from 'lib/lemon-ui/Link' -import { useEffect, useState } from 'react' +import { getExceptionAttributes, hasAnyInAppFrames, hasStacktrace } from 'scenes/error-tracking/utils' import { EventType } from '~/types' -import { CodeLine, getLanguage, Language } from '../CodeSnippet/CodeSnippet' import { stackFrameLogic } from './stackFrameLogic' -import { - ErrorTrackingException, - ErrorTrackingStackFrame, - ErrorTrackingStackFrameContext, - ErrorTrackingStackFrameContextLine, -} from './types' - -function StackTrace({ - frames, - showAllFrames, -}: { - frames: ErrorTrackingStackFrame[] - showAllFrames: boolean -}): JSX.Element | null { - const { stackFrameRecords } = useValues(stackFrameLogic) - const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) - - const panels = displayFrames.map( - ({ raw_id, source, line, column, resolved_name, lang, resolved, resolve_failure }, index) => { - const record = stackFrameRecords[raw_id] - return { - key: index, - header: ( -
-
- {source} - {resolved_name ? ( -
- in - {resolved_name} -
- ) : null} - {line ? ( -
- @ - - {line} - {column && `:${column}`} - -
- ) : null} -
- {!resolved && ( -
- - Unresolved - -
- )} -
- ), - content: - record && record.context ? ( - - ) : null, - className: 'p-0', - } - } - ) - - return -} - -function FrameContext({ - context, - language, -}: { - context: ErrorTrackingStackFrameContext - language: Language -}): JSX.Element { - const { before, line, after } = context - return ( - <> - - - - - ) -} - -function FrameContextLine({ - lines, - language, - highlight, -}: { - lines: ErrorTrackingStackFrameContextLine[] - language: Language - highlight?: boolean -}): JSX.Element { - return ( -
- {lines - .sort((l) => l.number) - .map(({ number, line }) => ( -
-
{number}
- -
- ))} -
- ) -} -function ChainedStackTraces({ exceptionList }: { exceptionList: ErrorTrackingException[] }): JSX.Element { - const hasAnyInApp = exceptionList.some(({ stacktrace }) => stacktrace?.frames?.some(({ in_app }) => in_app)) - const [showAllFrames, setShowAllFrames] = useState(!hasAnyInApp) - const { loadFromRawIds } = useActions(stackFrameLogic) - - useEffect(() => { - const frames: ErrorTrackingStackFrame[] = exceptionList.flatMap((e) => { - const trace = e.stacktrace - if (trace?.type === 'resolved') { - return trace.frames - } - return [] - }) - loadFromRawIds(frames.map(({ raw_id }) => raw_id)) - }, [exceptionList, loadFromRawIds]) - - return ( - <> -
-

Stack Trace

- {hasAnyInApp ? ( - { - setShowAllFrames(!showAllFrames) - }} - /> - ) : null} -
- {exceptionList.map(({ stacktrace, value }, index) => { - if (stacktrace && stacktrace.type === 'resolved') { - const { frames } = stacktrace - if (!showAllFrames && !frames?.some((frame) => frame.in_app)) { - // if we're not showing all frames and there are no in_app frames, skip this exception - return null - } - - return ( -
-

{value}

- -
- ) - } - })} - - ) -} - -export function getExceptionPropertiesFrom(eventProperties: Record): Record { - const { - $lib, - $lib_version, - $browser, - $browser_version, - $os, - $os_version, - $active_feature_flags, - $sentry_url, - $sentry_exception, - $level, - } = eventProperties - - let $exception_type = eventProperties.$exception_type - let $exception_message = eventProperties.$exception_message - let $exception_synthetic = eventProperties.$exception_synthetic - let $exception_list = eventProperties.$exception_list - - // exception autocapture sets $exception_list for all exceptions. - // If it's not present, then this is probably a sentry exception. Get this list from the sentry_exception - if (!$exception_list?.length && $sentry_exception) { - if (Array.isArray($sentry_exception.values)) { - $exception_list = $sentry_exception.values - } - } - - if (!$exception_type) { - $exception_type = $exception_list?.[0]?.type - } - if (!$exception_message) { - $exception_message = $exception_list?.[0]?.value - } - if ($exception_synthetic == undefined) { - $exception_synthetic = $exception_list?.[0]?.mechanism?.synthetic - } - - return { - $exception_type, - $exception_message, - $exception_synthetic, - $lib, - $lib_version, - $browser, - $browser_version, - $os, - $os_version, - $active_feature_flags, - $sentry_url, - $exception_list, - $level, - } -} +import { ChainedStackTraces } from './StackTraces' +import { ErrorTrackingException } from './types' export function ErrorDisplay({ eventProperties }: { eventProperties: EventType['properties'] }): JSX.Element { - const { - $exception_type, - $exception_message, - $exception_synthetic, - $lib, - $lib_version, - $browser, - $browser_version, - $os, - $os_version, - $sentry_url, - $exception_list, - $level, - } = getExceptionPropertiesFrom(eventProperties) + const { type, value, library, browser, os, sentryUrl, exceptionList, level, ingestionErrors, unhandled } = + getExceptionAttributes(eventProperties) - const exceptionList: ErrorTrackingException[] | undefined = $exception_list - const exceptionWithStack = exceptionList?.length && exceptionList.some((e) => !!e.stacktrace) - const ingestionErrors: string[] | undefined = eventProperties['$cymbal_errors'] + const exceptionWithStack = hasStacktrace(exceptionList) return ( -
-

{$exception_message}

+
+

{type || level}

+ {!exceptionWithStack &&
{value}
}
- {$exception_type || $level} Sentry @@ -261,10 +40,10 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' ) } /> - - - - + + + +
{ingestionErrors || exceptionWithStack ? : null} @@ -279,7 +58,29 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' )} - {exceptionWithStack ? : null} + {exceptionWithStack && }
) } + +const StackTrace = ({ exceptionList }: { exceptionList: ErrorTrackingException[] }): JSX.Element => { + const { showAllFrames } = useValues(stackFrameLogic) + const { setShowAllFrames } = useActions(stackFrameLogic) + const hasAnyInApp = hasAnyInAppFrames(exceptionList) + + return ( + <> +
+

Stack Trace

+ {hasAnyInApp ? ( + setShowAllFrames(!showAllFrames)} + /> + ) : null} +
+ + + ) +} diff --git a/frontend/src/lib/components/Errors/StackTraces.scss b/frontend/src/lib/components/Errors/StackTraces.scss new file mode 100644 index 0000000000000..c71349387cd11 --- /dev/null +++ b/frontend/src/lib/components/Errors/StackTraces.scss @@ -0,0 +1,25 @@ +.StackTrace { + .LemonCollapse { + &.LemonCollapse--embedded { + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + } + + .LemonCollapsePanel__header { + min-height: 1.875rem !important; + padding: 0 !important; + background-color: var(--accent-3000); + + &--disabled:hover { + background-color: var(--accent-3000) !important; + } + } + } + + &--embedded { + .StackTrace__type, + .StackTrace__value { + margin: 0 8px; + } + } +} diff --git a/frontend/src/lib/components/Errors/StackTraces.tsx b/frontend/src/lib/components/Errors/StackTraces.tsx new file mode 100644 index 0000000000000..588f6dae7039c --- /dev/null +++ b/frontend/src/lib/components/Errors/StackTraces.tsx @@ -0,0 +1,164 @@ +import './StackTraces.scss' + +import { LemonCollapse, Tooltip } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { useActions, useValues } from 'kea' +import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag' +import { useEffect } from 'react' + +import { CodeLine, getLanguage, Language } from '../CodeSnippet/CodeSnippet' +import { stackFrameLogic } from './stackFrameLogic' +import { + ErrorTrackingException, + ErrorTrackingStackFrame, + ErrorTrackingStackFrameContext, + ErrorTrackingStackFrameContextLine, +} from './types' + +export function ChainedStackTraces({ + exceptionList, + showAllFrames, + embedded = false, +}: { + exceptionList: ErrorTrackingException[] + showAllFrames: boolean + embedded?: boolean +}): JSX.Element { + const { loadFromRawIds } = useActions(stackFrameLogic) + + useEffect(() => { + const frames: ErrorTrackingStackFrame[] = exceptionList.flatMap((e) => { + const trace = e.stacktrace + if (trace?.type === 'resolved') { + return trace.frames + } + return [] + }) + loadFromRawIds(frames.map(({ raw_id }) => raw_id)) + }, [exceptionList, loadFromRawIds]) + + return ( + <> + {exceptionList.map(({ stacktrace, value, type }, index) => { + if (stacktrace && stacktrace.type === 'resolved') { + const { frames } = stacktrace + if (!showAllFrames && !frames?.some((frame) => frame.in_app)) { + // if we're not showing all frames and there are no in_app frames, skip this exception + return null + } + + return ( +
+
+

{type}

+
{value}
+
+ +
+ ) + } + })} + + ) +} + +function Trace({ + frames, + showAllFrames, + embedded, +}: { + frames: ErrorTrackingStackFrame[] + showAllFrames: boolean + embedded: boolean +}): JSX.Element | null { + const { stackFrameRecords } = useValues(stackFrameLogic) + const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) + + const panels = displayFrames.map( + ({ raw_id, source, line, column, resolved_name, lang, resolved, resolve_failure }, index) => { + const record = stackFrameRecords[raw_id] + return { + key: index, + header: ( +
+
+ {source} + {resolved_name ? ( +
+ in + {resolved_name} +
+ ) : null} + {line ? ( +
+ @ + + {line} + {column && `:${column}`} + +
+ ) : null} +
+ {!resolved && ( +
+ + Unresolved + +
+ )} +
+ ), + content: + record && record.context ? ( + + ) : null, + className: 'p-0', + } + } + ) + + return +} + +function FrameContext({ + context, + language, +}: { + context: ErrorTrackingStackFrameContext + language: Language +}): JSX.Element { + const { before, line, after } = context + return ( + <> + + + + + ) +} + +function FrameContextLine({ + lines, + language, + highlight, +}: { + lines: ErrorTrackingStackFrameContextLine[] + language: Language + highlight?: boolean +}): JSX.Element { + return ( +
+ {lines + .sort((l) => l.number) + .map(({ number, line }) => ( +
+
{number}
+ +
+ ))} +
+ ) +} diff --git a/frontend/src/lib/components/Errors/error-display.test.ts b/frontend/src/lib/components/Errors/error-display.test.ts index 2e9024e80b7b2..5c4d945ff7346 100644 --- a/frontend/src/lib/components/Errors/error-display.test.ts +++ b/frontend/src/lib/components/Errors/error-display.test.ts @@ -1,4 +1,4 @@ -import { getExceptionPropertiesFrom } from 'lib/components/Errors/ErrorDisplay' +import { getExceptionAttributes } from 'scenes/error-tracking/utils' describe('Error Display', () => { it('can read sentry stack trace when $exception_list is not present', () => { @@ -47,13 +47,11 @@ describe('Error Display', () => { $exception_personURL: 'https://app.posthog.com/person/f6kW3HXaha6dAvHZiOmgrcAXK09682P6nNPxvfjqM9c', $exception_type: 'Error', } - const result = getExceptionPropertiesFrom(eventProperties) + const result = getExceptionAttributes(eventProperties) expect(result).toEqual({ - $active_feature_flags: ['feature1,feature2'], - $browser: 'Chrome', - $browser_version: '92.0.4515', - $exception_message: 'There was an error creating the support ticket with zendesk.', - $exception_list: [ + browser: 'Chrome 92.0.4515', + value: 'There was an error creating the support ticket with zendesk.', + exceptionList: [ { mechanism: { handled: true, @@ -74,14 +72,14 @@ describe('Error Display', () => { value: 'There was an error creating the support ticket with zendesk.', }, ], - $exception_synthetic: undefined, - $exception_type: 'Error', - $lib: 'posthog-js', - $lib_version: '1.0.0', - $level: undefined, - $os: 'Windows', - $os_version: '10', - $sentry_url: + synthetic: undefined, + unhandled: false, + type: 'Error', + library: 'posthog-js 1.0.0', + level: undefined, + os: 'Windows 10', + ingestionErrors: undefined, + sentryUrl: 'https://sentry.io/organizations/posthog/issues/?project=1899813&query=40e442d79c22473391aeeeba54c82163', }) }) @@ -110,20 +108,19 @@ describe('Error Display', () => { $level: 'info', $exception_message: 'the message sent into sentry captureMessage', } - const result = getExceptionPropertiesFrom(eventProperties) + const result = getExceptionAttributes(eventProperties) expect(result).toEqual({ - $active_feature_flags: ['feature1,feature2'], - $browser: 'Chrome', - $browser_version: '92.0.4515', - $exception_message: 'the message sent into sentry captureMessage', - $exception_synthetic: undefined, - $exception_type: undefined, - $lib: 'posthog-js', - $lib_version: '1.0.0', - $level: 'info', - $os: 'Windows', - $os_version: '10', - $sentry_url: + browser: 'Chrome 92.0.4515', + value: 'the message sent into sentry captureMessage', + exceptionList: [], + ingestionErrors: undefined, + unhandled: true, + synthetic: undefined, + type: undefined, + library: 'posthog-js 1.0.0', + level: 'info', + os: 'Windows 10', + sentryUrl: 'https://sentry.io/organizations/posthog/issues/?project=1899813&query=40e442d79c22473391aeeeba54c82163', }) }) @@ -162,21 +159,17 @@ describe('Error Display', () => { ], $exception_personURL: 'https://app.posthog.com/person/f6kW3HXaha6dAvHZiOmgrcAXK09682P6nNPxvfjqM9c', } - const result = getExceptionPropertiesFrom(eventProperties) + const result = getExceptionAttributes(eventProperties) expect(result).toEqual({ - $active_feature_flags: ['feature1,feature2'], - $browser: 'Chrome', - $browser_version: '92.0.4515', - $exception_message: 'There was an error creating the support ticket with zendesk2.', - $exception_synthetic: false, - $exception_type: 'Error', - $lib: 'posthog-js', - $lib_version: '1.0.0', - $level: undefined, - $os: 'Windows', - $os_version: '10', - $sentry_url: undefined, - $exception_list: [ + browser: 'Chrome 92.0.4515', + value: 'There was an error creating the support ticket with zendesk2.', + synthetic: false, + type: 'Error', + library: 'posthog-js 1.0.0', + level: undefined, + os: 'Windows 10', + sentryUrl: undefined, + exceptionList: [ { mechanism: { handled: true, @@ -198,6 +191,8 @@ describe('Error Display', () => { value: 'There was an error creating the support ticket with zendesk2.', }, ], + ingestionErrors: undefined, + unhandled: false, }) }) }) diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.ts b/frontend/src/lib/components/Errors/stackFrameLogic.ts index 41516ccee257f..ae4668410b747 100644 --- a/frontend/src/lib/components/Errors/stackFrameLogic.ts +++ b/frontend/src/lib/components/Errors/stackFrameLogic.ts @@ -1,4 +1,4 @@ -import { actions, kea, path } from 'kea' +import { actions, kea, path, reducers } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' @@ -20,8 +20,19 @@ export const stackFrameLogic = kea([ actions({ loadFromRawIds: (rawIds: ErrorTrackingStackFrame['raw_id'][]) => ({ rawIds }), loadForSymbolSet: (symbolSetId: ErrorTrackingSymbolSet['id']) => ({ symbolSetId }), + setShowAllFrames: (showAllFrames: boolean) => ({ showAllFrames }), }), + reducers(() => ({ + showAllFrames: [ + false, + { persist: true }, + { + setShowAllFrames: (_, { showAllFrames }) => showAllFrames, + }, + ], + })), + loaders(({ values }) => ({ stackFrameRecords: [ {} as KeyedStackFrameRecords, diff --git a/frontend/src/lib/components/Errors/types.ts b/frontend/src/lib/components/Errors/types.ts index 3442e81884eb8..fe2ca33e222c4 100644 --- a/frontend/src/lib/components/Errors/types.ts +++ b/frontend/src/lib/components/Errors/types.ts @@ -3,6 +3,11 @@ export interface ErrorTrackingException { module: string type: string value: string + mechanism?: { + synthetic?: boolean + handled?: boolean + type: 'generic' + } } interface ErrorTrackingRawStackTrace { diff --git a/frontend/src/lib/components/PanelLayout/PanelLayout.tsx b/frontend/src/lib/components/PanelLayout/PanelLayout.tsx new file mode 100644 index 0000000000000..8cf0c74531f86 --- /dev/null +++ b/frontend/src/lib/components/PanelLayout/PanelLayout.tsx @@ -0,0 +1,135 @@ +import { IconMinus } from '@posthog/icons' +import { LemonButton, LemonButtonProps, Tooltip } from '@posthog/lemon-ui' +import clsx from 'clsx' +import { LemonMenu, LemonMenuItem, LemonMenuProps } from 'lib/lemon-ui/LemonMenu/LemonMenu' +import { useState } from 'react' + +type PanelContainerProps = { + children: React.ReactNode + primary: boolean + className?: string + column?: boolean + title: string + header?: JSX.Element | null +} + +interface SettingsMenuProps extends Omit { + label?: string + items: LemonMenuItem[] + icon?: JSX.Element + isAvailable?: boolean + whenUnavailable?: LemonMenuItem + highlightWhenActive?: boolean + closeOnClickInside?: boolean +} + +type SettingsButtonProps = Omit & { + tooltip?: string + icon?: JSX.Element | null + label?: JSX.Element | string +} + +type SettingsToggleProps = SettingsButtonProps & { + active: boolean +} + +function PanelLayout({ className, ...props }: Omit): JSX.Element { + return +} + +function Container({ children, primary, className, column }: Omit): JSX.Element { + return ( +
+ {children} +
+ ) +} + +function Panel({ children, primary, className, title, header }: Omit): JSX.Element { + const [open, setOpen] = useState(true) + + return ( +
+
+ {title} +
+ {header} + setOpen(!open)} icon={} /> +
+
+ {open ? children : null} +
+ ) +} + +export function SettingsMenu({ + label, + items, + icon, + isAvailable = true, + closeOnClickInside = true, + highlightWhenActive = true, + whenUnavailable, + ...props +}: SettingsMenuProps): JSX.Element { + const active = items.some((cf) => !!cf.active) + return ( + + + {label} + + + ) +} + +export function SettingsToggle({ tooltip, icon, label, active, ...props }: SettingsToggleProps): JSX.Element { + const button = ( + + {label} + + ) + + // otherwise the tooltip shows instead of the disabled reason + return props.disabledReason ? button : {button} +} + +export function SettingsButton(props: SettingsButtonProps): JSX.Element { + return +} + +PanelLayout.Panel = Panel +PanelLayout.Container = Container +PanelLayout.SettingsMenu = SettingsMenu +PanelLayout.SettingsToggle = SettingsToggle +PanelLayout.SettingsButton = SettingsButton + +export default PanelLayout diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 12e4193fe35dc..5cfb22d0b4d93 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -564,12 +564,6 @@ export function DataTable({ }, rowExpandable: ({ result }) => !!result, noIndent: true, - expandedRowClassName: ({ result }) => { - const record = Array.isArray(result) ? result[0] : result - return record && record['event'] === '$exception' - ? 'border border-x-danger-dark bg-danger-highlight' - : null - }, } : undefined } @@ -578,7 +572,10 @@ export function DataTable({ 'DataTable__row--highlight_once': result && highlightedRows.has(result), 'DataTable__row--category_row': !!label, 'border border-x-danger-dark bg-danger-highlight': - result && result[0] && result[0]['event'] === '$exception', + sourceFeatures.has(QueryFeature.highlightExceptionEventRows) && + result && + result[0] && + result[0]['event'] === '$exception', }) } footer={ diff --git a/frontend/src/queries/nodes/DataTable/queryFeatures.ts b/frontend/src/queries/nodes/DataTable/queryFeatures.ts index af5659d40223b..74c42831e0a18 100644 --- a/frontend/src/queries/nodes/DataTable/queryFeatures.ts +++ b/frontend/src/queries/nodes/DataTable/queryFeatures.ts @@ -26,6 +26,7 @@ export enum QueryFeature { displayResponseError, hideLoadNextButton, testAccountFilters, + highlightExceptionEventRows, } export function getQueryFeatures(query: Node): Set { diff --git a/frontend/src/queries/nodes/HogQLX/render.tsx b/frontend/src/queries/nodes/HogQLX/render.tsx index f07dedd62d046..13803d82fcdd0 100644 --- a/frontend/src/queries/nodes/HogQLX/render.tsx +++ b/frontend/src/queries/nodes/HogQLX/render.tsx @@ -51,6 +51,7 @@ export function renderHogQLX(value: any): JSX.Element { data-attr="hog-ql-view-recording-button" className="inline-block" {...props} + disabledReason={sessionId ? undefined : 'No session id associated with this event'} /> ) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 6a90fcf65b470..028544987411e 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -5404,6 +5404,9 @@ "description": { "type": ["string", "null"] }, + "earliest": { + "type": "string" + }, "first_seen": { "format": "date-time", "type": "string" @@ -5442,6 +5445,7 @@ "users", "first_seen", "last_seen", + "earliest", "assignee", "status" ], diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 736037ad3db03..58e779914b070 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1974,6 +1974,7 @@ export interface ErrorTrackingIssue { first_seen: string /** @format date-time */ last_seen: string + earliest: string // Sparkline data handled by the DataTable volume?: any assignee: number | null diff --git a/frontend/src/scenes/activity/explore/EventDetails.tsx b/frontend/src/scenes/activity/explore/EventDetails.tsx index 60f59f338f618..d91db2b4ae75b 100644 --- a/frontend/src/scenes/activity/explore/EventDetails.tsx +++ b/frontend/src/scenes/activity/explore/EventDetails.tsx @@ -124,7 +124,7 @@ export function EventDetails({ event, tableProps }: EventDetailsProps): JSX.Elem key: 'exception', label: 'Exception', content: ( -
+
), diff --git a/frontend/src/scenes/activity/explore/EventsScene.tsx b/frontend/src/scenes/activity/explore/EventsScene.tsx index 3e140d4405eeb..5f2b3288e5419 100644 --- a/frontend/src/scenes/activity/explore/EventsScene.tsx +++ b/frontend/src/scenes/activity/explore/EventsScene.tsx @@ -1,5 +1,6 @@ import { useActions, useValues } from 'kea' +import { QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' import { Query } from '~/queries/Query/Query' import { eventsSceneLogic } from './eventsSceneLogic' @@ -8,5 +9,14 @@ export function EventsScene(): JSX.Element { const { query } = useValues(eventsSceneLogic) const { setQuery } = useActions(eventsSceneLogic) - return + return ( + + ) } diff --git a/frontend/src/scenes/error-tracking/ErrorTracking.scss b/frontend/src/scenes/error-tracking/ErrorTracking.scss index 2a981bfdbf970..e0b502862ef6e 100644 --- a/frontend/src/scenes/error-tracking/ErrorTracking.scss +++ b/frontend/src/scenes/error-tracking/ErrorTracking.scss @@ -2,3 +2,22 @@ height: calc(100vh - 12rem); min-height: 25rem; } + +.PanelLayout.ErrorTrackingPanelLayout { + --error-tracking-panel-layout-height: calc( + 100vh - var(--breadcrumbs-height-full) - var(--scene-padding) - var(--scene-padding-bottom) + ); + + height: var(--error-tracking-panel-layout-height); + min-height: var(--error-tracking-panel-layout-height); + max-height: var(--error-tracking-panel-layout-height); + + & .PanelLayout__container--secondary { + min-width: 300px; + max-width: 300px; + } + + & .SessionRecordingPlayer { + height: 24rem; + } +} diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx index 62c3ee10fa4be..9cbb649a812ce 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx @@ -12,21 +12,14 @@ import { TestAccountFilter } from 'scenes/insights/filters/TestAccountFilter' import { errorTrackingLogic } from './errorTrackingLogic' import { errorTrackingSceneLogic } from './errorTrackingSceneLogic' -export const FilterGroup = (): JSX.Element => { - const { filterGroup, filterTestAccounts, searchQuery } = useValues(errorTrackingLogic) - const { setFilterGroup, setFilterTestAccounts, setSearchQuery } = useActions(errorTrackingLogic) +export const FilterGroup = ({ children }: { children?: React.ReactNode }): JSX.Element => { + const { filterGroup, filterTestAccounts } = useValues(errorTrackingLogic) + const { setFilterGroup, setFilterTestAccounts } = useActions(errorTrackingLogic) return (
- + {children} { ) } -export const Options = ({ isIssue = false }: { isIssue?: boolean }): JSX.Element => { +export const Options = (): JSX.Element => { const { dateRange, assignee } = useValues(errorTrackingLogic) const { setDateRange, setAssignee } = useActions(errorTrackingLogic) const { orderBy } = useValues(errorTrackingSceneLogic) @@ -106,58 +99,71 @@ export const Options = ({ isIssue = false }: { isIssue?: boolean }): JSX.Element size="small" />
- {!isIssue && ( -
- Sort by: - -
- )} +
+ Sort by: + +
- {!isIssue && ( - <> - Assigned to: - { - setAssignee(user?.id || null) - }} - /> - - )} + <> + Assigned to: + { + setAssignee(user?.id || null) + }} + /> +
) } +export const UniversalSearch = (): JSX.Element => { + const { searchQuery } = useValues(errorTrackingLogic) + const { setSearchQuery } = useActions(errorTrackingLogic) + + return ( + + ) +} + export default { FilterGroup, Options, + UniversalSearch, } diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx index 7171ff1d5ef35..db95a1da046f9 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingIssueScene.tsx @@ -1,6 +1,6 @@ import './ErrorTracking.scss' -import { LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { LemonButton, LemonTabs, Spinner } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' import { useEffect } from 'react' @@ -10,10 +10,9 @@ import { ErrorTrackingIssue } from '~/queries/schema' import { AlphaAccessScenePrompt } from './AlphaAccessScenePrompt' import { AssigneeSelect } from './AssigneeSelect' -import ErrorTrackingFilters from './ErrorTrackingFilters' -import { errorTrackingIssueSceneLogic } from './errorTrackingIssueSceneLogic' -import { OverviewTab } from './groups/OverviewTab' -import { SymbolSetUploadModal } from './SymbolSetUploadModal' +import { errorTrackingIssueSceneLogic, IssueTab } from './errorTrackingIssueSceneLogic' +import { EventsTab } from './issue/tabs/EventsTab' +import { OverviewTab } from './issue/tabs/OverviewTab' export const scene: SceneExport = { component: ErrorTrackingIssueScene, @@ -29,8 +28,8 @@ const STATUS_LABEL: Record = { } export function ErrorTrackingIssueScene(): JSX.Element { - const { issue, issueLoading } = useValues(errorTrackingIssueSceneLogic) - const { updateIssue, loadIssue } = useActions(errorTrackingIssueSceneLogic) + const { issue, issueLoading, tab } = useValues(errorTrackingIssueSceneLogic) + const { updateIssue, loadIssue, setTab } = useActions(errorTrackingIssueSceneLogic) useEffect(() => { // don't like doing this but scene logics do not unmount after being loaded @@ -81,11 +80,27 @@ export function ErrorTrackingIssueScene(): JSX.Element { ) } /> - - - - - + {issue ? ( + , + }, + + { + key: IssueTab.Events, + label: 'Events', + content: , + }, + ]} + onChange={setTab} + /> + ) : ( + + )} ) diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx index 81bd16fbafa2b..e573d41bc0415 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx @@ -57,7 +57,9 @@ export function ErrorTrackingScene(): JSX.Element {
- + + + {selectedIssueIds.length === 0 ? : } diff --git a/frontend/src/scenes/error-tracking/errorTrackingIssueSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingIssueSceneLogic.ts index 01ec5e7aecfce..6d58a1b8220f9 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingIssueSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingIssueSceneLogic.ts @@ -1,8 +1,8 @@ -import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { actions, connect, kea, key, path, props, reducers, selectors } from 'kea' import { loaders } from 'kea-loaders' import { actionToUrl, router, urlToAction } from 'kea-router' import api from 'lib/api' -import { Dayjs, dayjs } from 'lib/dayjs' +import { Dayjs } from 'lib/dayjs' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' @@ -31,6 +31,7 @@ export interface ErrorTrackingIssueSceneLogicProps { export enum IssueTab { Overview = 'overview', + Events = 'events', Breakdowns = 'breakdowns', } @@ -93,43 +94,6 @@ export const errorTrackingIssueSceneLogic = kea issue, }, ], - events: [ - [] as ErrorTrackingEvent[], - { - loadEvents: async () => { - const response = await api.query( - errorTrackingIssueEventsQuery({ - select: ['uuid', 'properties', 'timestamp', 'person'], - issueId: props.id, - dateRange: values.dateRange, - filterTestAccounts: values.filterTestAccounts, - filterGroup: values.filterGroup, - offset: values.events.length, - }) - ) - - const newResults = response.results.map((r) => ({ - uuid: r[0], - properties: JSON.parse(r[1]), - timestamp: dayjs(r[2]), - person: r[3], - })) - - return [...values.events, ...newResults] - }, - }, - ], - })), - - listeners(({ values, actions }) => ({ - loadIssueSuccess: () => { - actions.loadEvents() - }, - loadEventsSuccess: () => { - if (!values.activeEventUUID) { - actions.setActiveEventUUID(values.events[0]?.uuid) - } - }, })), selectors({ @@ -150,16 +114,29 @@ export const errorTrackingIssueSceneLogic = kea [p.id, s.dateRange, s.filterTestAccounts, s.filterGroup], + (id, dateRange, filterTestAccounts, filterGroup) => + errorTrackingIssueEventsQuery({ + issueId: id, + dateRange: dateRange, + filterTestAccounts: filterTestAccounts, + filterGroup: filterGroup, + }), + ], + + issueProperties: [(s) => [s.issue], (issue): Record => (issue ? JSON.parse(issue.earliest) : {})], }), actionToUrl(({ values }) => ({ setTab: () => { const searchParams = router.values.searchParams - - if (values.tab != IssueTab.Overview) { + if (values.tab == IssueTab.Overview) { + delete searchParams['tab'] + } else { searchParams['tab'] = values.tab } - return [router.values.location.pathname, searchParams] }, })), diff --git a/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx b/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx deleted file mode 100644 index d449562c41281..0000000000000 --- a/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { PersonDisplay, TZLabel } from '@posthog/apps-common' -import clsx from 'clsx' -import { useActions, useValues } from 'kea' -import { EmptyMessage } from 'lib/components/EmptyMessage/EmptyMessage' -import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' -import { Playlist } from 'lib/components/Playlist/Playlist' -import ViewRecordingButton, { mightHaveRecording } from 'lib/components/ViewRecordingButton' -import { PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordingPreview' - -import { ErrorTrackingEvent, errorTrackingIssueSceneLogic } from '../errorTrackingIssueSceneLogic' - -export const OverviewTab = (): JSX.Element => { - const { events, issueLoading, eventsLoading, activeEventUUID } = useValues(errorTrackingIssueSceneLogic) - const { loadEvents, setActiveEventUUID } = useActions(errorTrackingIssueSceneLogic) - - return ( -
-
- ({ ...e, id: e.uuid })), - render: ListItemException, - }, - ]} - onSelect={({ uuid }) => { - setActiveEventUUID(uuid) - }} - activeItemId={activeEventUUID} - listEmptyState={
No exceptions found
} - content={({ activeItem: event }) => - event ? ( -
-
- -
-
- -
-
- ) : ( - - ) - } - onScrollListEdge={(edge) => { - if (edge === 'bottom' && !eventsLoading) { - loadEvents() - } - }} - /> -
-
- ) -} - -const ListItemException = ({ - item: { timestamp, properties, person }, - isActive, -}: { - item: ErrorTrackingEvent - isActive: boolean -}): JSX.Element => { - const recordingProperties = ['$browser', '$device_type', '$os'] - .flatMap((property) => { - let value = properties[property] - const label = value - if (property === '$device_type') { - value = properties['$device_type'] || properties['$initial_device_type'] - } - - return { property, value, label } - }) - .filter((property) => !!property.value) - - return ( -
-
-
- -
- -
- {properties.$current_url &&
{properties.$current_url}
} - -
- ) -} diff --git a/frontend/src/scenes/error-tracking/issue/panels/MetaPanel.tsx b/frontend/src/scenes/error-tracking/issue/panels/MetaPanel.tsx new file mode 100644 index 0000000000000..a42ff4ef965c9 --- /dev/null +++ b/frontend/src/scenes/error-tracking/issue/panels/MetaPanel.tsx @@ -0,0 +1,38 @@ +import { TZLabel } from '@posthog/apps-common' +import { LemonSkeleton } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic' + +export const MetaPanel = (): JSX.Element => { + const { issue } = useValues(errorTrackingIssueSceneLogic) + + return ( +
+ {issue ?
{issue.description}
: } +
+
+
First seen
+ {issue ? : } +
+
+
Last seen
+ {issue ? : } +
+
+
+
+
Occurrences
+
{issue?.occurrences}
+
+
+
Sessions
+
{issue?.sessions}
+
+
+
Users
+
{issue?.users}
+
+
+
+ ) +} diff --git a/frontend/src/scenes/error-tracking/issue/panels/OverviewPanel.tsx b/frontend/src/scenes/error-tracking/issue/panels/OverviewPanel.tsx new file mode 100644 index 0000000000000..7c767db84619e --- /dev/null +++ b/frontend/src/scenes/error-tracking/issue/panels/OverviewPanel.tsx @@ -0,0 +1,42 @@ +import { useValues } from 'kea' +import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic' +import { getExceptionAttributes } from 'scenes/error-tracking/utils' + +export const OverviewPanel = (): JSX.Element => { + const { issueProperties } = useValues(errorTrackingIssueSceneLogic) + + const { synthetic, level, browser, os, library, unhandled } = getExceptionAttributes(issueProperties) + + const TableRow = ({ label, value }: { label: string; value: string | undefined }): JSX.Element => ( + + {label} + {value ??
unknown
} + + ) + + return ( +
+
+ + {[ + { label: 'Level', value: level }, + { label: 'Synthetic', value: synthetic }, + { label: 'Library', value: library }, + { label: 'Unhandled', value: unhandled }, + ].map((row, index) => ( + + ))} +
+ + {[ + { label: 'Browser', value: browser }, + { label: 'OS', value: os }, + { label: 'URL', value: issueProperties['$current_url'] }, + ].map((row, index) => ( + + ))} +
+
+
+ ) +} diff --git a/frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx b/frontend/src/scenes/error-tracking/issue/tabs/BreakdownsTab.tsx similarity index 95% rename from frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx rename to frontend/src/scenes/error-tracking/issue/tabs/BreakdownsTab.tsx index 7db79ebc29a72..35f427033b4be 100644 --- a/frontend/src/scenes/error-tracking/groups/BreakdownsTab.tsx +++ b/frontend/src/scenes/error-tracking/issue/tabs/BreakdownsTab.tsx @@ -6,8 +6,8 @@ import { useState } from 'react' import { Query } from '~/queries/Query/Query' -import { errorTrackingLogic } from '../errorTrackingLogic' -import { errorTrackingIssueBreakdownQuery } from '../queries' +import { errorTrackingLogic } from '../../errorTrackingLogic' +import { errorTrackingIssueBreakdownQuery } from '../../queries' const gridColumnsMap = { small: 'grid-cols-1', diff --git a/frontend/src/scenes/error-tracking/issue/tabs/EventsTab.tsx b/frontend/src/scenes/error-tracking/issue/tabs/EventsTab.tsx new file mode 100644 index 0000000000000..42a02c7094c1d --- /dev/null +++ b/frontend/src/scenes/error-tracking/issue/tabs/EventsTab.tsx @@ -0,0 +1,35 @@ +import { LemonDivider } from '@posthog/lemon-ui' +import { useValues } from 'kea' +import ErrorTrackingFilters from 'scenes/error-tracking/ErrorTrackingFilters' +import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic' + +import { Query } from '~/queries/Query/Query' +import { QueryContext } from '~/queries/types' +import { InsightLogicProps } from '~/types' + +export const EventsTab = (): JSX.Element => { + const { eventsQuery } = useValues(errorTrackingIssueSceneLogic) + + const insightProps: InsightLogicProps = { + dashboardItemId: 'new-ErrorTrackingEventsQuery', + } + + const context: QueryContext = { + columns: { + 'recording_button(properties.$session_id)': { + title: 'Recording', + width: '132px', + }, + }, + showOpenEditorButton: false, + insightProps: insightProps, + } + + return ( + <> + + + + + ) +} diff --git a/frontend/src/scenes/error-tracking/issue/tabs/OverviewTab.tsx b/frontend/src/scenes/error-tracking/issue/tabs/OverviewTab.tsx new file mode 100644 index 0000000000000..b086f95cd1a7f --- /dev/null +++ b/frontend/src/scenes/error-tracking/issue/tabs/OverviewTab.tsx @@ -0,0 +1,76 @@ +import { useActions, useValues } from 'kea' +import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic' +import { ChainedStackTraces } from 'lib/components/Errors/StackTraces' +import PanelLayout from 'lib/components/PanelLayout/PanelLayout' +import { errorTrackingIssueSceneLogic } from 'scenes/error-tracking/errorTrackingIssueSceneLogic' +import { getExceptionAttributes, hasAnyInAppFrames, hasStacktrace } from 'scenes/error-tracking/utils' +import { SessionRecordingPlayer } from 'scenes/session-recordings/player/SessionRecordingPlayer' + +import { MetaPanel } from '../panels/MetaPanel' +import { OverviewPanel } from '../panels/OverviewPanel' + +export const OverviewTab = (): JSX.Element => { + const { showAllFrames } = useValues(stackFrameLogic) + const { issueProperties } = useValues(errorTrackingIssueSceneLogic) + const { setShowAllFrames } = useActions(stackFrameLogic) + + const { exceptionList } = getExceptionAttributes(issueProperties) + const exceptionWithStack = hasStacktrace(exceptionList) + const hasAnyInApp = hasAnyInAppFrames(exceptionList) + const hasRecording = issueProperties['$session_id'] && issueProperties['$recording_status'] === 'active' + + return ( + + + + + + {exceptionWithStack && ( + setShowAllFrames(!showAllFrames)} + /> + ) : null + } + > +
+ +
+
+ )} + {hasRecording && ( + + + + )} +
+ + + + + +
+ ) +} diff --git a/frontend/src/scenes/error-tracking/queries.ts b/frontend/src/scenes/error-tracking/queries.ts index 781c8d75fa4e6..50ea3b38fcfd1 100644 --- a/frontend/src/scenes/error-tracking/queries.ts +++ b/frontend/src/scenes/error-tracking/queries.ts @@ -138,20 +138,20 @@ export const errorTrackingIssueQuery = ({ } export const errorTrackingIssueEventsQuery = ({ - select, issueId, dateRange, filterTestAccounts, filterGroup, - offset, }: { - select: string[] issueId: ErrorTrackingIssue['id'] dateRange: DateRange filterTestAccounts: boolean filterGroup: UniversalFiltersGroup - offset: number -}): EventsQuery => { +}): DataTableNode => { + // const select = ['person', 'timestamp', 'recording_button(properties.$session_id)'] + // row expansion only works when you fetch the entire event with '*' + const columns = ['*', 'person', 'timestamp', 'recording_button(properties.$session_id)'] + const group = filterGroup.values[0] as UniversalFiltersGroup const properties = group.values as AnyPropertyFilter[] @@ -159,25 +159,30 @@ export const errorTrackingIssueEventsQuery = ({ // associated with issues that have been merged into this primary issue const where = [`'${issueId}' == properties.$exception_issue_id`] - const query: EventsQuery = { + const eventsQuery: EventsQuery = { kind: NodeKind.EventsQuery, event: '$exception', - select, + select: columns, where, properties, filterTestAccounts: filterTestAccounts, - offset: offset, - limit: 50, } if (dateRange.date_from) { - query.after = dateRange.date_from + eventsQuery.after = dateRange.date_from } if (dateRange.date_to) { - query.before = dateRange.date_to + eventsQuery.before = dateRange.date_to } - return query + return { + kind: NodeKind.DataTableNode, + source: eventsQuery, + showActions: false, + showTimings: false, + columns: columns, + expandable: true, + } } export const errorTrackingIssueBreakdownQuery = ({ diff --git a/frontend/src/scenes/error-tracking/utils.test.ts b/frontend/src/scenes/error-tracking/utils.test.ts index b360f23df6bd6..3eb7dd72c36f1 100644 --- a/frontend/src/scenes/error-tracking/utils.test.ts +++ b/frontend/src/scenes/error-tracking/utils.test.ts @@ -15,6 +15,7 @@ describe('mergeIssues', () => { sessions: 100, status: 'active', users: 50, + earliest: '', volume: [ '__hx_tag', 'Sparkline', @@ -43,6 +44,7 @@ describe('mergeIssues', () => { sessions: 5, status: 'active', users: 1, + earliest: '', volume: [ '__hx_tag', 'Sparkline', @@ -69,6 +71,7 @@ describe('mergeIssues', () => { sessions: 1, status: 'active', users: 1, + earliest: '', volume: [ '__hx_tag', 'Sparkline', @@ -95,6 +98,7 @@ describe('mergeIssues', () => { sessions: 500, status: 'active', users: 50, + earliest: '', volume: [ '__hx_tag', 'Sparkline', @@ -119,6 +123,7 @@ describe('mergeIssues', () => { id: 'primaryId', assignee: 400, description: 'This is the original description', + earliest: '', name: 'TypeError', status: 'active', // earliest first_seen diff --git a/frontend/src/scenes/error-tracking/utils.ts b/frontend/src/scenes/error-tracking/utils.ts index 5ef545f005a98..ae868fd8985bd 100644 --- a/frontend/src/scenes/error-tracking/utils.ts +++ b/frontend/src/scenes/error-tracking/utils.ts @@ -1,3 +1,4 @@ +import { ErrorTrackingException } from 'lib/components/Errors/types' import { dayjs } from 'lib/dayjs' import { ErrorTrackingIssue } from '~/queries/schema' @@ -40,3 +41,70 @@ export const mergeIssues = ( volume: volume, } } + +export function getExceptionAttributes( + properties: Record +): { ingestionErrors?: string[]; exceptionList: ErrorTrackingException[] } & Record< + 'type' | 'value' | 'synthetic' | 'library' | 'browser' | 'os' | 'sentryUrl' | 'level' | 'unhandled', + any +> { + const { + $lib, + $lib_version, + $browser: browser, + $browser_version: browserVersion, + $os: os, + $os_version: osVersion, + $sentry_url: sentryUrl, + $sentry_exception, + $level: level, + $cymbal_errors: ingestionErrors, + } = properties + + let type = properties.$exception_type + let value = properties.$exception_message + let synthetic: boolean | undefined = properties.$exception_synthetic + let exceptionList: ErrorTrackingException[] | undefined = properties.$exception_list + + // exception autocapture sets $exception_list for all exceptions. + // If it's not present, then this is probably a sentry exception. Get this list from the sentry_exception + if (!exceptionList?.length && $sentry_exception) { + if (Array.isArray($sentry_exception.values)) { + exceptionList = $sentry_exception.values + } + } + + if (!type) { + type = exceptionList?.[0]?.type + } + if (!value) { + value = exceptionList?.[0]?.value + } + if (synthetic == undefined) { + synthetic = exceptionList?.[0]?.mechanism?.synthetic + } + + const handled = exceptionList?.[0]?.mechanism?.handled ?? false + + return { + type, + value, + synthetic, + library: `${$lib} ${$lib_version}`, + browser: browser ? `${browser} ${browserVersion}` : undefined, + os: os ? `${os} ${osVersion}` : undefined, + sentryUrl, + exceptionList: exceptionList || [], + unhandled: !handled, + level, + ingestionErrors, + } +} + +export function hasStacktrace(exceptionList: ErrorTrackingException[]): boolean { + return exceptionList?.length > 0 && exceptionList.some((e) => !!e.stacktrace) +} + +export function hasAnyInAppFrames(exceptionList: ErrorTrackingException[]): boolean { + return exceptionList.some(({ stacktrace }) => stacktrace?.frames?.some(({ in_app }) => in_app)) +} diff --git a/posthog/hogql_queries/error_tracking_query_runner.py b/posthog/hogql_queries/error_tracking_query_runner.py index 88f66050e52e7..091665bf7caba 100644 --- a/posthog/hogql_queries/error_tracking_query_runner.py +++ b/posthog/hogql_queries/error_tracking_query_runner.py @@ -54,6 +54,10 @@ def select(self): ), ast.Alias(alias="last_seen", expr=ast.Call(name="max", args=[ast.Field(chain=["timestamp"])])), ast.Alias(alias="first_seen", expr=ast.Call(name="min", args=[ast.Field(chain=["timestamp"])])), + ast.Alias( + alias="earliest", + expr=ast.Call(name="argMin", args=[ast.Field(chain=["properties"]), ast.Field(chain=["timestamp"])]), + ), ast.Alias(alias="id", expr=ast.Field(chain=["issue_id"])), ] diff --git a/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr b/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr index 20abb16f555dd..2a6cf10f4f8f7 100644 --- a/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr +++ b/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr @@ -6,6 +6,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -34,6 +35,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -79,6 +81,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -124,6 +127,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -169,6 +173,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -197,6 +202,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -226,6 +232,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -255,6 +262,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN @@ -300,6 +308,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, + argMin(events.properties, toTimeZone(events.timestamp, 'UTC')) AS earliest, if(not(empty(events__exception_issue_override.issue_id)), events__exception_issue_override.issue_id, accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), 'UUID')) AS id FROM events LEFT OUTER JOIN diff --git a/posthog/hogql_queries/test/test_error_tracking_query_runner.py b/posthog/hogql_queries/test/test_error_tracking_query_runner.py index 6e15387162643..91b40b4fda255 100644 --- a/posthog/hogql_queries/test/test_error_tracking_query_runner.py +++ b/posthog/hogql_queries/test/test_error_tracking_query_runner.py @@ -1,7 +1,5 @@ from unittest import TestCase from freezegun import freeze_time -from datetime import datetime -from zoneinfo import ZoneInfo from dateutil.relativedelta import relativedelta from django.utils.timezone import now @@ -287,6 +285,7 @@ def test_column_names(self): "users", "last_seen", "first_seen", + "earliest", "id", ], ) @@ -310,6 +309,7 @@ def test_column_names(self): "users", "last_seen", "first_seen", + "earliest", "id", ], ) @@ -518,38 +518,15 @@ def test_overrides_aggregation(self): ) results = self._calculate(runner)["results"] - self.assertEqual( - results, - [ - { - "id": self.issue_id_one, - "name": None, - "description": None, - "assignee": None, - "volume": None, - "status": ErrorTrackingIssue.Status.ACTIVE, - "first_seen": datetime(2020, 1, 10, 9, 11, tzinfo=ZoneInfo("UTC")), - "last_seen": datetime(2020, 1, 10, 11, 11, tzinfo=ZoneInfo("UTC")), - # count is (2 x issue_one) + (1 x issue_three) - "occurrences": 3, - "sessions": 1, - "users": 2, - }, - { - "id": self.issue_id_two, - "name": None, - "description": None, - "assignee": None, - "volume": None, - "status": ErrorTrackingIssue.Status.ACTIVE, - "first_seen": datetime(2020, 1, 10, 10, 11, tzinfo=ZoneInfo("UTC")), - "last_seen": datetime(2020, 1, 10, 10, 11, tzinfo=ZoneInfo("UTC")), - "occurrences": 1, - "sessions": 1, - "users": 1, - }, - ], - ) + + self.assertEqual(len(results), 2) + + # count is (2 x issue_one) + (1 x issue_three) + self.assertEqual(results[0]["id"], self.issue_id_one) + self.assertEqual(results[0]["occurrences"], 3) + + self.assertEqual(results[1]["id"], self.issue_id_two) + self.assertEqual(results[1]["occurrences"], 1) @snapshot_clickhouse_queries def test_assignee_groups(self): diff --git a/posthog/schema.py b/posthog/schema.py index 2dc9969cc38be..dda86c2fe74bf 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -746,6 +746,7 @@ class ErrorTrackingIssue(BaseModel): ) assignee: Optional[float] = None description: Optional[str] = None + earliest: str first_seen: AwareDatetime id: str last_seen: AwareDatetime diff --git a/rust/cymbal/src/hack/js_data.rs b/rust/cymbal/src/hack/js_data.rs index cb5ed93187092..d698829bfcb8f 100644 --- a/rust/cymbal/src/hack/js_data.rs +++ b/rust/cymbal/src/hack/js_data.rs @@ -4,7 +4,7 @@ use thiserror::Error; use crate::symbol_store::sourcemap::OwnedSourceMapCache; -// NOTE: see psothog/api/error_tracking.py +// NOTE: see posthog/api/error_tracking.py pub struct JsData { data: Vec, // For legacy reasons, before we has this serialisation format,