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 50d668e3f3bf6..a147e1eb54571 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 2f226c7c05f95..81e997f168c75 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--importing-module--dark.png b/frontend/__snapshots__/components-errors-error-display--importing-module--dark.png index 67811b05acb58..ab946e6ae69b9 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--importing-module--dark.png and b/frontend/__snapshots__/components-errors-error-display--importing-module--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--importing-module--light.png b/frontend/__snapshots__/components-errors-error-display--importing-module--light.png index 046a20b8c3935..6ac99e959b670 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--importing-module--light.png and b/frontend/__snapshots__/components-errors-error-display--importing-module--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--dark.png b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--dark.png index 3885c148fece1..0312bb569ce05 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--dark.png and b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--light.png b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--light.png index a63f72eabf26c..7d1583f3cd647 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--light.png and b/frontend/__snapshots__/components-errors-error-display--resize-observer-loop-limit-exceeded--light.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--safari-script-error--dark.png b/frontend/__snapshots__/components-errors-error-display--safari-script-error--dark.png index 6b2259c04bc1c..c7dd6a65c1b7a 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--safari-script-error--dark.png and b/frontend/__snapshots__/components-errors-error-display--safari-script-error--dark.png differ diff --git a/frontend/__snapshots__/components-errors-error-display--safari-script-error--light.png b/frontend/__snapshots__/components-errors-error-display--safari-script-error--light.png index 43b0d6d315ac9..cf2e13d4c3a4d 100644 Binary files a/frontend/__snapshots__/components-errors-error-display--safari-script-error--light.png and b/frontend/__snapshots__/components-errors-error-display--safari-script-error--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 new file mode 100644 index 0000000000000..30563222db743 Binary files /dev/null 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 new file mode 100644 index 0000000000000..7bd93910cad21 Binary files /dev/null and b/frontend/__snapshots__/components-errors-error-display--stack-trace-with-line-context--light.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--group-page--dark.png b/frontend/__snapshots__/scenes-app-errortracking--group-page--dark.png index 38d5bc497d4e4..40140ddc7eaf2 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--group-page--dark.png and b/frontend/__snapshots__/scenes-app-errortracking--group-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png b/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png index 10b0e390935f4..002eed214b8e2 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png and b/frontend/__snapshots__/scenes-app-errortracking--group-page--light.png differ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 774bf3522ad3a..b443065a41e9d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -113,6 +113,7 @@ import { } from '~/types' import { AlertType, AlertTypeWrite } from './components/Alerts/types' +import { ErrorTrackingStackFrameContext } from './components/Errors/types' import { ACTIVITY_PAGE_SIZE, DashboardPrivilegeLevel, @@ -719,8 +720,12 @@ class ApiRequest { return this.errorTracking().addPathComponent('upload_source_maps') } - public errorTrackingStackFrames(ids: string[]): ApiRequest { - return this.errorTracking().addPathComponent('stack_frames').withQueryString({ ids }) + public errorTrackingStackFrames(): ApiRequest { + return this.errorTracking().addPathComponent('stack_frames') + } + + public errorTrackingStackFrameContexts(ids: string[]): ApiRequest { + return this.errorTrackingStackFrames().addPathComponent('contexts').withQueryString(toParams({ ids }, true)) } // # Warehouse @@ -1862,8 +1867,8 @@ const api = { return await new ApiRequest().errorTrackingUploadSourceMaps().create({ data }) }, - async fetchStackFrames(ids: string[]): Promise<{ content: string }> { - return await new ApiRequest().errorTrackingStackFrames(ids).get() + async fetchStackFrames(ids: string[]): Promise> { + return await new ApiRequest().errorTrackingStackFrameContexts(ids).get() }, }, diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss index a23cdf21970c5..62352529bb29e 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.scss @@ -26,7 +26,6 @@ pre { padding: 0.75rem 1rem; - margin: 0; // Reset background: var(--accent-3000) !important; border: solid 1px var(--border-3000) !important; border-radius: var(--radius) !important; diff --git a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx index cf7033aff1046..8bbf88f3bebf0 100644 --- a/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx +++ b/frontend/src/lib/components/CodeSnippet/CodeSnippet.tsx @@ -54,6 +54,53 @@ export enum Language { Kotlin = 'kotlin', } +export const getLanguage = (lang: string): Language => { + switch (lang) { + case 'bash': + return Language.Bash + case 'jsx': + return Language.JSX + case 'javascript': + return Language.JavaScript + case 'java': + return Language.Java + case 'ruby': + return Language.Ruby + case 'objectivec': + return Language.ObjectiveC + case 'swift': + return Language.Swift + case 'elixir': + return Language.Elixir + case 'php': + return Language.PHP + case 'python': + return Language.Python + case 'dart': + return Language.Dart + case 'go': + return Language.Go + case 'json': + return Language.JSON + case 'yaml': + return Language.YAML + case 'html': + return Language.HTML + case 'xml': + return Language.XML + case 'http': + return Language.HTTP + case 'markup': + return Language.Markup + case 'sql': + return Language.SQL + case 'kotlin': + return Language.Kotlin + default: + return Language.Text + } +} + SyntaxHighlighter.registerLanguage(Language.Bash, bash) SyntaxHighlighter.registerLanguage(Language.JSX, jsx) SyntaxHighlighter.registerLanguage(Language.JavaScript, javascript) @@ -98,8 +145,6 @@ export function CodeSnippet({ thing = 'snippet', maxLinesWithoutExpansion, }: CodeSnippetProps): JSX.Element | null { - const { isDarkModeOn } = useValues(themeLogic) - const [expanded, setExpanded] = useState(false) const [indexOfLimitNewline, setIndexOfLimitNewline] = useState( maxLinesWithoutExpansion ? indexOfNth(text || '', '\n', maxLinesWithoutExpansion) : -1 @@ -136,14 +181,7 @@ export function CodeSnippet({ noPadding /> - - {displayedText} - + {indexOfLimitNewline !== -1 && ( setExpanded(!expanded)} @@ -163,6 +201,30 @@ export function CodeSnippet({ ) } +export function CodeLine({ + text, + wrapLines, + language, +}: { + text: string + wrapLines: boolean + language: Language +}): JSX.Element { + const { isDarkModeOn } = useValues(themeLogic) + + return ( +
{children}
} + > + {text} +
+ ) +} + function indexOfNth(string: string, character: string, n: number): number { let count = 0, indexSoFar = 0 diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx index 7e327618e48d0..6107628852b9e 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.stories.tsx @@ -1,11 +1,45 @@ import { Meta } from '@storybook/react' import { ErrorDisplay } from 'lib/components/Errors/ErrorDisplay' +import { mswDecorator } from '~/mocks/browser' import { EventType } from '~/types' const meta: Meta = { title: 'Components/Errors/Error Display', component: ErrorDisplay, + decorators: [ + mswDecorator({ + get: { + '/api/projects/:team_id/error_tracking/stack_frames/contexts/': { + rawId: { + before: [ + { + number: 7, + line: ' const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app)', + }, + { + number: 8, + line: '', + }, + { number: 9, line: ' useEffect(() => {' }, + ], + line: { number: 10, line: ' loadFrameContexts({ frames })' }, + after: [ + { number: 11, line: ' }, [frames, loadFrameContexts])' }, + { + number: 12, + line: '', + }, + { + number: 13, + line: ' const initiallyActiveIndex = displayFrames.findIndex((f) => f.in_app) || 0', + }, + ], + }, + }, + }, + }), + ], } export default meta @@ -55,13 +89,14 @@ function errorProperties(properties: Record): EventType['properties synthetic: true, }, stacktrace: { + type: 'resolved', frames: [ { - colno: 0, - filename: 'https://app.posthog.com/home', - function: '?', + column: 0, + source: 'https://app.posthog.com/home', + resolved_name: '?', in_app: true, - lineno: 0, + line: 0, }, ], }, @@ -141,13 +176,14 @@ export function AnonymousErrorWithStackTrace(): JSX.Element { type: 'Error', value: 'wat123', stacktrace: { + type: 'resolved', frames: [ { - filename: '', - function: '?', + source: '', + resolved_name: '?', in_app: true, - lineno: 1, - colno: 26, + line: 1, + column: 26, }, ], }, @@ -172,48 +208,14 @@ export function ChainedErrorStack(): JSX.Element { stacktrace: { frames: [ { - filename: 'example2.py', - abs_path: '/posthog-python/example2.py', - function: 'will_raise', - module: '__main__', - lineno: 33, - pre_context: [ - 'def more_obfuscation():', - ' print(3 / 0)', - '', - 'def will_raise():', - ' try:', - ], - context_line: ' more_obfuscation()', - post_context: [ - ' except Exception as e:', - ' raise CustomException("This is a custom exception") from e', - '', - 'will_raise()', - 'exit()', - ], + source: '/posthog-python/example2.py', + resolved_name: 'will_raise', + line: 33, }, { - filename: 'example2.py', - abs_path: '/posthog-python/example2.py', - function: 'more_obfuscation', - module: '__main__', - lineno: 29, - pre_context: [ - '', - 'class CustomException(Exception):', - ' pass', - '', - 'def more_obfuscation():', - ], - context_line: ' print(3 / 0)', - post_context: [ - '', - 'def will_raise():', - ' try:', - ' more_obfuscation()', - ' except Exception as e:', - ], + source: '/posthog-python/example2.py', + resolved_name: 'more_obfuscation', + line: 29, }, ], }, @@ -225,42 +227,43 @@ export function ChainedErrorStack(): JSX.Element { stacktrace: { frames: [ { - filename: 'example2.py', - abs_path: '/Users/neilkakkar/Project/posthog-python/example2.py', - function: '', - module: '__main__', - lineno: 37, - pre_context: [ - ' try:', - ' more_obfuscation()', - ' except Exception as e:', - ' raise CustomException("This is a custom exception") from e', - '', - ], - context_line: 'will_raise()', - post_context: [ - 'exit()', - '', - '', - '# print(posthog.get_all_flags("distinct_id_random_22"))', - '# print(', - ], + source: '/Users/neilkakkar/Project/posthog-python/example2.py', + resolved_name: '', + line: 37, }, { - filename: 'example2.py', - abs_path: '/Users/neilkakkar/Project/posthog-python/example2.py', - function: 'will_raise', - module: '__main__', - lineno: 35, - pre_context: [ - '', - 'def will_raise():', - ' try:', - ' more_obfuscation()', - ' except Exception as e:', - ], - context_line: ' raise CustomException("This is a custom exception") from e', - post_context: ['', 'will_raise()', 'exit()', '', ''], + source: '/Users/neilkakkar/Project/posthog-python/example2.py', + resolved_name: 'will_raise', + line: 35, + }, + ], + }, + }, + ], + })} + /> + ) +} + +export function StackTraceWithLineContext(): JSX.Element { + return ( + ', + resolved_name: '?', + in_app: true, + line: 1, + column: 26, + lang: 'javascript', }, ], }, diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.tsx index 604b80fcf6820..de1e0858be5a2 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.tsx @@ -1,67 +1,121 @@ import './ErrorDisplay.scss' import { IconFlag } from '@posthog/icons' -import { LemonCollapse } from '@posthog/lemon-ui' +import { LemonBanner, LemonCollapse } 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 { useState } from 'react' +import { useEffect, useState } from 'react' import { EventType } from '~/types' -import { StackFrame } from './stackFrameLogic' +import { CodeLine, getLanguage, Language } from '../CodeSnippet/CodeSnippet' +import { stackFrameLogic } from './stackFrameLogic' +import { + ErrorTrackingException, + ErrorTrackingStackFrame, + ErrorTrackingStackFrameContext, + ErrorTrackingStackFrameContextLine, +} from './types' -interface RawStackTrace { - type: 'raw' - frames: StackFrame[] -} -interface ResolvedStackTrace { - type: 'resolved' - frames: StackFrame[] -} +function StackTrace({ + frames, + showAllFrames, +}: { + frames: ErrorTrackingStackFrame[] + showAllFrames: boolean +}): JSX.Element | null { + const { frameContexts } = useValues(stackFrameLogic) + const { loadFrameContexts } = useActions(stackFrameLogic) + const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) -interface Exception { - stacktrace: ResolvedStackTrace | RawStackTrace - module: string - type: string - value: string -} + useEffect(() => { + loadFrameContexts({ frames }) + }, [frames, loadFrameContexts]) -function StackTrace({ frames, showAllFrames }: { frames: StackFrame[]; showAllFrames: boolean }): JSX.Element | null { - const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) + const initiallyActiveIndex = displayFrames.findIndex((f) => f.in_app) || 0 - const panels = displayFrames.map(({ filename, lineno, colno, function: functionName }, index) => { + const panels = displayFrames.map(({ raw_id, source, line, column, resolved_name, lang }, index) => { + const frameContext = frameContexts[raw_id] return { key: index, header: (
- {filename} - {functionName ? ( + {source} + {resolved_name ? (
in - {functionName} + {resolved_name}
) : null} - {lineno && colno ? ( + {line ? (
- at line + @ - {lineno}:{colno} + {line} + {column && `:${column}`}
) : null}
), - content: null, + content: frameContext ? : null, + className: 'p-0', } }) - return + return +} + +function FrameContext({ + context, + language, +}: { + context: ErrorTrackingStackFrameContext + language: Language +}): JSX.Element { + const { before, line, after } = context + return ( + <> + + + + + ) } -function ChainedStackTraces({ exceptionList }: { exceptionList: Exception[] }): JSX.Element { +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, + ingestionErrors, +}: { + exceptionList: ErrorTrackingException[] + ingestionErrors: string[] +}): JSX.Element { const [showAllFrames, setShowAllFrames] = useState(false) return ( @@ -77,19 +131,32 @@ function ChainedStackTraces({ exceptionList }: { exceptionList: Exception[] }): }} /> + {ingestionErrors && ( + +
    + {ingestionErrors.map((e, i) => ( +
  • {e}
  • + ))} +
+
+ )} {exceptionList.map(({ stacktrace, value }, index) => { - 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 + if (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}

+ +
+ ) } - return ( -
-

{value}

- -
- ) + return null })} ) @@ -186,6 +253,7 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' $sentry_url, $exception_list, $level, + $cymbal_errors, } = getExceptionPropertiesFrom(eventProperties) return ( @@ -217,7 +285,9 @@ export function ErrorDisplay({ eventProperties }: { eventProperties: EventType[' - {$exception_list?.length ? : null} + {$exception_list?.length ? ( + + ) : null}

Active Feature Flags

diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.ts b/frontend/src/lib/components/Errors/stackFrameLogic.ts new file mode 100644 index 0000000000000..ab1744481c0a8 --- /dev/null +++ b/frontend/src/lib/components/Errors/stackFrameLogic.ts @@ -0,0 +1,25 @@ +import { kea, path } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import type { stackFrameLogicType } from './stackFrameLogicType' +import { ErrorTrackingStackFrame, ErrorTrackingStackFrameContext } from './types' + +export const stackFrameLogic = kea([ + path(['components', 'Errors', 'stackFrameLogic']), + loaders(({ values }) => ({ + frameContexts: [ + {} as Record, + { + loadFrameContexts: async ({ frames }: { frames: ErrorTrackingStackFrame[] }) => { + const loadedFrameIds = Object.keys(values.frameContexts) + const ids = frames + .filter(({ raw_id }) => loadedFrameIds.includes(raw_id)) + .map(({ raw_id }) => raw_id) + const response = await api.errorTracking.fetchStackFrames(ids) + return { ...values.frameContexts, ...response } + }, + }, + ], + })), +]) diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.tsx b/frontend/src/lib/components/Errors/stackFrameLogic.tsx deleted file mode 100644 index 3852055d12bbb..0000000000000 --- a/frontend/src/lib/components/Errors/stackFrameLogic.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { kea, path } from 'kea' -import { loaders } from 'kea-loaders' -import api from 'lib/api' - -import type { stackFrameLogicType } from './stackFrameLogicType' - -export interface StackFrame { - filename: string - lineno: number - colno: number - function: string - in_app?: boolean -} - -export const stackFrameLogic = kea([ - path(['components', 'Errors', 'stackFrameLogic']), - loaders(({ values }) => ({ - stackFrames: [ - {} as Record, - { - loadFrames: async ({ frameIds }: { frameIds: string[] }) => { - const loadedFrameIds = Object.keys(values.stackFrames) - const ids = frameIds.filter((id) => loadedFrameIds.includes(id)) - await api.errorTracking.fetchStackFrames(ids) - return {} - }, - }, - ], - })), -]) diff --git a/frontend/src/lib/components/Errors/types.ts b/frontend/src/lib/components/Errors/types.ts new file mode 100644 index 0000000000000..df359ada3b16e --- /dev/null +++ b/frontend/src/lib/components/Errors/types.ts @@ -0,0 +1,54 @@ +export interface ErrorTrackingException { + stacktrace: ErrorTrackingRawStackTrace | ErrorTrackingResolvedStackTrace + module: string + type: string + value: string +} + +interface ErrorTrackingRawStackTrace { + type: 'raw' + frames: any[] // TODO: type more concretely if we end up needing this (right now we show the $cymbal_errors instead) +} +interface ErrorTrackingResolvedStackTrace { + type: 'resolved' + frames: ErrorTrackingStackFrame[] +} + +export interface ErrorTrackingStackFrameRecord { + id: string + raw_id: string + created_at: string + symbol_set: string + resolved: boolean + context: ErrorTrackingStackFrameContext | null + contents: ErrorTrackingStackFrame // For now, while we're not 100% on content structure +} + +export type ErrorTrackingStackFrameContext = { + before: ErrorTrackingStackFrameContextLine[] + line: ErrorTrackingStackFrameContextLine + after: ErrorTrackingStackFrameContextLine[] +} +export type ErrorTrackingStackFrameContextLine = { number: number; line: string } + +export interface ErrorTrackingStackFrame { + raw_id: string + mangled_name: string + line: number | null + column: number | null + source: string | null + in_app: boolean + resolved_name: string | null + lang: string + resolved: boolean + resolve_failure: string | null +} + +export interface ErrorTrackingSymbolSet { + id: string + ref: string + team_id: number + created_at: string + storage_ptr: string | null + failure_reason: string | null +} diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index b488304ab64bb..233a0df72703e 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -28,6 +28,7 @@ dead_letter_queue, debug_ch_queries, early_access_feature, + error_tracking, event_definition, exports, feature_flag, @@ -506,6 +507,13 @@ def register_grandfathered_environment_nested_viewset( # ["team_id"], # ) +projects_router.register( + r"error_tracking/stack_frames", + error_tracking.ErrorTrackingStackFrameViewSet, + "project_error_tracking_stack_frames", + ["project_id"], +) + projects_router.register( r"comments", comments.CommentViewSet, diff --git a/posthog/api/error_tracking.py b/posthog/api/error_tracking.py index 339cb61e59437..8cb8d6d102148 100644 --- a/posthog/api/error_tracking.py +++ b/posthog/api/error_tracking.py @@ -1,5 +1,11 @@ import structlog +from rest_framework import viewsets, response, serializers +from posthog.api.routing import TeamAndOrgViewSetMixin +from .forbid_destroy_model import ForbidDestroyModel + +from posthog.api.utils import action +from posthog.models.error_tracking import ErrorTrackingStackFrame FIFTY_MEGABYTES = 50 * 1024 * 1024 @@ -55,3 +61,23 @@ class ObjectStorageUnavailable(Exception): # code="object_storage_required", # detail="Object storage must be available to allow source map uploads.", # ) + + +class ErrorTrackingStackFrameSerializer(serializers.ModelSerializer): + class Meta: + model = ErrorTrackingStackFrame + fields = ["raw_id", "context"] + + +class ErrorTrackingStackFrameViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ReadOnlyModelViewSet): + scope_object = "INTERNAL" + queryset = ErrorTrackingStackFrame.objects.all() + serializer_class = ErrorTrackingStackFrameSerializer + + @action(methods=["GET"], detail=False) + def contexts(self, request, **kwargs) -> response.Response: + ids = request.GET.getlist("ids", []) + queryset = self.filter_queryset(self.queryset.filter(team=self.team, raw_id__in=ids)) + serializer = self.get_serializer(queryset, many=True) + keyed_data = {frame["raw_id"]: frame["context"] for frame in serializer.data} + return response.Response(keyed_data)