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..6b87e46af5867 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..05dcc87b58322 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..eaf1166d78b81 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..aabcaef42777b 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..aea268e6d8daa 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..2722b5c280693 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..938afe444d07e 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..3514c0557398f 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__/scenes-app-errortracking--group-page--dark.png b/frontend/__snapshots__/scenes-app-errortracking--group-page--dark.png index 38d5bc497d4e4..1cd42428a44f1 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..331f3feabf0b2 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..832f52cc3aba5 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 { ErrorTrackingStackFrameRecord, ErrorTrackingSymbolSet } from './components/Errors/types' import { ACTIVITY_PAGE_SIZE, DashboardPrivilegeLevel, @@ -723,6 +724,14 @@ class ApiRequest { return this.errorTracking().addPathComponent('stack_frames').withQueryString({ ids }) } + public symbolSets(): ApiRequest { + return this.errorTracking().withAction('symbol_sets') + } + + public symbolSetStackFrames(symbolSetId: string): ApiRequest { + return this.symbolSets().withAction(symbolSetId).withAction('stack_frames') + } + // # Warehouse public dataWarehouseTables(teamId?: TeamType['id']): ApiRequest { return this.projectsDetail(teamId).addPathComponent('warehouse_tables') @@ -1865,6 +1874,18 @@ const api = { async fetchStackFrames(ids: string[]): Promise<{ content: string }> { return await new ApiRequest().errorTrackingStackFrames(ids).get() }, + + async fetchSymbolSets(): Promise { + return await api.loadPaginatedResults(new ApiRequest().symbolSets().assembleFullUrl()) + }, + + async fetchSymbolSetStackFrames(symbolSetId: string): Promise { + return await new ApiRequest().symbolSetStackFrames(symbolSetId).get() + }, + + async fetchSymbolSet(symbolSetId: string): Promise { + return await new ApiRequest().symbolSets().withAction(symbolSetId).get() + }, }, recordings: { diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.tsx index 604b80fcf6820..0f7b33feca4b3 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.tsx @@ -11,15 +11,15 @@ import { useState } from 'react' import { EventType } from '~/types' -import { StackFrame } from './stackFrameLogic' +import { ErrorTrackingStackFrame } from './types' interface RawStackTrace { type: 'raw' - frames: StackFrame[] + frames: ErrorTrackingStackFrame[] } interface ResolvedStackTrace { type: 'resolved' - frames: StackFrame[] + frames: ErrorTrackingStackFrame[] } interface Exception { @@ -29,26 +29,33 @@ interface Exception { value: string } -function StackTrace({ frames, showAllFrames }: { frames: StackFrame[]; showAllFrames: boolean }): JSX.Element | null { +function StackTrace({ + frames, + showAllFrames, +}: { + frames: ErrorTrackingStackFrame[] + showAllFrames: boolean +}): JSX.Element | null { const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) - const panels = displayFrames.map(({ filename, lineno, colno, function: functionName }, index) => { + // TODO - this doesn't account for nulls + const panels = displayFrames.map(({ source, line, column, resolved_name: functionName }, index) => { return { key: index, header: (
- {filename} + {source} {functionName ? (
in {functionName}
) : null} - {lineno && colno ? ( + {line && column ? (
at line - {lineno}:{colno} + {line}:{column}
) : null} diff --git a/frontend/src/lib/components/Errors/SymbolSetDisplay.tsx b/frontend/src/lib/components/Errors/SymbolSetDisplay.tsx new file mode 100644 index 0000000000000..401752ff80aee --- /dev/null +++ b/frontend/src/lib/components/Errors/SymbolSetDisplay.tsx @@ -0,0 +1,116 @@ +import { LemonCollapse, Spinner } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { useEffect, useState } from 'react' + +import { symbolSetLogic } from './symbolSetLogic' +import { ErrorTrackingStackFrameRecord, ErrorTrackingSymbolSet } from './types' + +function StackFrameDisplay({ frame }: { frame: ErrorTrackingStackFrameRecord }): JSX.Element { + const { contents } = frame + + return ( + + {contents.source || 'Unknown source'} + {contents.resolved_name && ( +
+ in + {contents.resolved_name} +
+ )} + {contents.line && ( +
+ at line + + {contents.line}:{contents.column || 0} + +
+ )} +
+ ), + content: frame.context &&
{frame.context}
, // TODO - this needs to account for structure context later + }, + ]} + /> + ) +} + +function SymbolSetDisplay({ symbolSet }: { symbolSet: ErrorTrackingSymbolSet }): JSX.Element { + const [expanded, setExpanded] = useState(false) + const { symbolSetStackFrames, symbolSetStackFramesLoading } = useValues(symbolSetLogic) + const { loadStackFrames } = useActions(symbolSetLogic) + + useEffect(() => { + if (expanded && !symbolSetStackFrames[symbolSet.id]) { + loadStackFrames({ symbolSetId: symbolSet.id }) + } + }, [expanded, symbolSet.id, loadStackFrames, symbolSetStackFrames]) + + return ( + +
+

{symbolSet.ref}

+ {symbolSet.failure_reason && ( +
Failed: {symbolSet.failure_reason}
+ )} +
+
Storage: {symbolSet.storage_ptr || 'Not stored'}
+ + ), + content: ( +
+ {symbolSetStackFramesLoading ? ( +
+ +
+ ) : ( + symbolSetStackFrames[symbolSet.id]?.map((frame: ErrorTrackingStackFrameRecord) => ( + + )) + )} +
+ ), + }, + ]} + onChange={(key) => setExpanded(!!key)} + /> + ) +} + +export function SymbolSetsDisplay(): JSX.Element { + const { symbolSets, symbolSetsLoading } = useValues(symbolSetLogic) + const { loadSymbolSets } = useActions(symbolSetLogic) + + useEffect(() => { + loadSymbolSets() + }, [loadSymbolSets]) + + if (symbolSetsLoading) { + return ( +
+ +
+ ) + } + + if (!symbolSets?.length) { + return
No symbol sets found
+ } + + return ( +
+ {symbolSets.map((symbolSet) => ( + + ))} +
+ ) +} diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.tsx b/frontend/src/lib/components/Errors/stackFrameLogic.tsx index 3852055d12bbb..ae1d5adbf6654 100644 --- a/frontend/src/lib/components/Errors/stackFrameLogic.tsx +++ b/frontend/src/lib/components/Errors/stackFrameLogic.tsx @@ -3,20 +3,13 @@ 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 -} +import { ErrorTrackingStackFrame } from './types' export const stackFrameLogic = kea([ path(['components', 'Errors', 'stackFrameLogic']), loaders(({ values }) => ({ stackFrames: [ - {} as Record, + {} as Record, { loadFrames: async ({ frameIds }: { frameIds: string[] }) => { const loadedFrameIds = Object.keys(values.stackFrames) diff --git a/frontend/src/lib/components/Errors/symbolSetLogic.tsx b/frontend/src/lib/components/Errors/symbolSetLogic.tsx new file mode 100644 index 0000000000000..3a83b8dc460cc --- /dev/null +++ b/frontend/src/lib/components/Errors/symbolSetLogic.tsx @@ -0,0 +1,32 @@ +import { kea, path } from 'kea' +import { loaders } from 'kea-loaders' +import api from 'lib/api' + +import type { symbolSetLogicType } from './symbolSetLogicType' +import { ErrorTrackingStackFrameRecord, ErrorTrackingSymbolSet } from './types' + +export const symbolSetLogic = kea([ + path(['components', 'Errors', 'symbolSetLogic']), + loaders(({ values }) => ({ + symbolSets: [ + [] as ErrorTrackingSymbolSet[], + { + loadSymbolSets: async () => { + return await api.errorTracking.fetchSymbolSets() + }, + }, + ], + symbolSetStackFrames: [ + {} as Record, + { + loadStackFrames: async ({ symbolSetId }: { symbolSetId: string }) => { + const frames = await api.errorTracking.fetchSymbolSetStackFrames(symbolSetId) + return { + ...values.symbolSetStackFrames, + [symbolSetId]: frames, + } + }, + }, + ], + })), +]) diff --git a/frontend/src/lib/components/Errors/types.ts b/frontend/src/lib/components/Errors/types.ts new file mode 100644 index 0000000000000..fb41707eedbf2 --- /dev/null +++ b/frontend/src/lib/components/Errors/types.ts @@ -0,0 +1,31 @@ +export interface ErrorTrackingStackFrameRecord { + id: string + raw_id: string + created_at: string + symbol_set: string + resolved: boolean + context: string | null // TODO - switch this to the structure we've discussed once the migration is merged + contents: ErrorTrackingStackFrame // For now, while we're not 100% on content structure +} + +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/plugin-server/src/worker/ingestion/event-pipeline/runner.ts b/plugin-server/src/worker/ingestion/event-pipeline/runner.ts index 25b3d77d128b3..7f12b2d84a259 100644 --- a/plugin-server/src/worker/ingestion/event-pipeline/runner.ts +++ b/plugin-server/src/worker/ingestion/event-pipeline/runner.ts @@ -259,7 +259,7 @@ export class EventPipelineRunner { event.team_id ) - if (event.event === '$exception' && event.team_id == 2) { + if (event.event === '$exception') { const [exceptionAck] = await this.runStep( produceExceptionSymbolificationEventStep, [this, rawEvent], diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index b488304ab64bb..76a3c68e4094b 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, @@ -499,12 +500,12 @@ def register_grandfathered_environment_nested_viewset( ["project_id"], ) -# projects_router.register( -# r"error_tracking", -# error_tracking.ErrorTrackingGroupViewSet, -# "project_error_tracking", -# ["team_id"], -# ) +projects_router.register( + r"error_tracking/symbol_sets", + error_tracking.ErrorTrackingSymbolSetViewSet, + "project_error_tracking_symbol_sets", + ["project_id"], +) projects_router.register( r"comments", diff --git a/posthog/api/error_tracking.py b/posthog/api/error_tracking.py index 339cb61e59437..1b7dd0df126b0 100644 --- a/posthog/api/error_tracking.py +++ b/posthog/api/error_tracking.py @@ -1,57 +1,48 @@ -import structlog - - -FIFTY_MEGABYTES = 50 * 1024 * 1024 - -logger = structlog.get_logger(__name__) - - -class ObjectStorageUnavailable(Exception): - pass - - -# class ErrorTrackingGroupSerializer(serializers.ModelSerializer): -# class Meta: -# model = ErrorTrackingGroup -# fields = ["assignee", "status"] - - -# class ErrorTrackingGroupViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ModelViewSet): -# scope_object = "INTERNAL" -# queryset = ErrorTrackingGroup.objects.all() -# serializer_class = ErrorTrackingGroupSerializer - -# def safely_get_object(self, queryset) -> QuerySet: -# stringified_fingerprint = self.kwargs["pk"] -# fingerprint = json.loads(urlsafe_base64_decode(stringified_fingerprint)) -# group, _ = queryset.get_or_create(fingerprint=fingerprint, team=self.team) -# return group - -# @action(methods=["POST"], detail=True) -# def merge(self, request, **kwargs): -# group: ErrorTrackingGroup = self.get_object() -# merging_fingerprints: list[list[str]] = request.data.get("merging_fingerprints", []) -# group.merge(merging_fingerprints) -# return Response({"success": True}) - -# @action(methods=["POST"], detail=False) -# def upload_source_maps(self, request, **kwargs): -# try: -# if settings.OBJECT_STORAGE_ENABLED: -# file = request.FILES["source_map"] -# if file.size > FIFTY_MEGABYTES: -# raise ValidationError(code="file_too_large", detail="Source maps must be less than 50MB") - -# upload_path = ( -# f"{settings.OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER}/team-{self.team_id}/{file.name}" -# ) - -# object_storage.write(upload_path, file) -# return Response({"ok": True}, status=status.HTTP_201_CREATED) -# else: -# raise ObjectStorageUnavailable() -# except ObjectStorageUnavailable: -# raise ValidationError( -# code="object_storage_required", -# detail="Object storage must be available to allow source map uploads.", -# ) +from rest_framework import serializers, viewsets, mixins +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import QuerySet + +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.models.error_tracking.error_tracking import ErrorTrackingSymbolSet, ErrorTrackingStackFrame + + +class ErrorTrackingStackFrameSerializer(serializers.ModelSerializer): + class Meta: + model = ErrorTrackingStackFrame + fields = ["id", "raw_id", "created_at", "contents", "resolved", "context"] + + +class ErrorTrackingSymbolSetSerializer(serializers.ModelSerializer): + class Meta: + model = ErrorTrackingSymbolSet + fields = ["id", "ref", "team_id", "created_at", "storage_ptr", "failure_reason"] + read_only_fields = ["team_id"] + + +class ErrorTrackingSymbolSetViewSet( + TeamAndOrgViewSetMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.DestroyModelMixin, + viewsets.GenericViewSet, +): + scope_object = "query" + serializer_class = ErrorTrackingSymbolSetSerializer + queryset = ErrorTrackingSymbolSet.objects.all() + + scope_object_read_actions = ["list", "retrieve", "stack_frames"] + + def safely_get_queryset(self, queryset: QuerySet) -> QuerySet: + return queryset.filter(team_id=self.team.id) + + @action(methods=["GET"], detail=True) + def stack_frames(self, request, *args, **kwargs): + symbol_set = self.get_object() + frames = ErrorTrackingStackFrame.objects.filter(symbol_set=symbol_set, team_id=self.team.id) + serializer = ErrorTrackingStackFrameSerializer(frames, many=True) + return Response(serializer.data) + + def perform_destroy(self, instance): + # The related stack frames will be deleted via CASCADE + instance.delete() diff --git a/posthog/api/test/__snapshots__/test_api_docs.ambr b/posthog/api/test/__snapshots__/test_api_docs.ambr index 8105280e6a538..9d7cba2f0ca42 100644 --- a/posthog/api/test/__snapshots__/test_api_docs.ambr +++ b/posthog/api/test/__snapshots__/test_api_docs.ambr @@ -32,6 +32,7 @@ '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard.py: Warning [DashboardsViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard.Dashboard" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/dashboards/dashboard_templates.py: Warning [DashboardTemplateViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.dashboard_templates.DashboardTemplate" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/early_access_feature.py: Warning [EarlyAccessFeatureViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.early_access_feature.EarlyAccessFeature" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', + '/home/runner/work/posthog/posthog/posthog/api/error_tracking.py: Warning [ErrorTrackingSymbolSetViewSet]: could not derive type of path parameter "project_id" because model "posthog.models.error_tracking.error_tracking.ErrorTrackingSymbolSet" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/event.py: Warning [EventViewSet]: could not derive type of path parameter "id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', '/home/runner/work/posthog/posthog/posthog/api/event.py: Warning [EventViewSet]: could not derive type of path parameter "project_id" because it is untyped and obtaining queryset from the viewset failed. Consider adding a type to the path (e.g. ) or annotating the parameter type with @extend_schema. Defaulting to "string".', "/home/runner/work/posthog/posthog/posthog/api/event_definition.py: Error [EventDefinitionViewSet]: exception raised while getting serializer. Hint: Is get_serializer_class() returning None or is get_queryset() not working without a request? Ignoring the view for now. (Exception: 'AnonymousUser' object has no attribute 'organization')", diff --git a/rust/cymbal/src/bin/generate_test_events.rs b/rust/cymbal/src/bin/generate_test_events.rs index 2e237a056d7c9..9be475f6ea967 100644 --- a/rust/cymbal/src/bin/generate_test_events.rs +++ b/rust/cymbal/src/bin/generate_test_events.rs @@ -7,6 +7,7 @@ use cymbal::{ }; use envconfig::Envconfig; use health::HealthRegistry; +use uuid::Uuid; const EXCEPTION_DATA: &str = include_str!("../../tests/static/raw_ch_exception_list.json"); @@ -19,13 +20,20 @@ async fn main() { .await; let producer = create_kafka_producer(&config, handle).await.unwrap(); - let exception: ClickHouseEvent = serde_json::from_str(EXCEPTION_DATA).unwrap(); - let exceptions = (0..10000).map(|_| exception.clone()).collect::>(); + let mut exception: ClickHouseEvent = serde_json::from_str(EXCEPTION_DATA).unwrap(); + exception.team_id = 1; + exception.project_id = 1; + let exceptions = (0..100).map(|_| exception.clone()).collect::>(); get_props(&exception).unwrap(); loop { println!("Sending {} exception kafka", exceptions.len()); - send_iter_to_kafka(&producer, "exception_symbolification_events", &exceptions) + let to_send = exceptions.iter().map(|e| { + let mut e = e.clone(); + e.uuid = Uuid::now_v7(); + e + }); + send_iter_to_kafka(&producer, "exception_symbolification_events", to_send) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_secs(1)).await; diff --git a/rust/cymbal/src/bin/run.sh b/rust/cymbal/src/bin/run.sh index 694c0f9e5d843..e274454ca8b42 100755 --- a/rust/cymbal/src/bin/run.sh +++ b/rust/cymbal/src/bin/run.sh @@ -4,4 +4,4 @@ export OBJECT_STORAGE_BUCKET="posthog" export OBJECT_STORAGE_ACCESS_KEY_ID="object_storage_root_user" export OBJECT_STORAGE_SECRET_ACCESS_KEY="object_storage_root_password" -cargo run --bin cymbal +RUST_LOG=info cargo run --bin cymbal