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 a147e1eb54571..ad6a6015f6de3 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 81e997f168c75..5d1a2a2b6e81a 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 ab946e6ae69b9..24e5802e6b897 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 6ac99e959b670..642ac564d5288 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 0312bb569ce05..7f47cba096432 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 7d1583f3cd647..b8f15ee5a2161 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 c7dd6a65c1b7a..2129c06bb26e0 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 cf2e13d4c3a4d..18885b5e19b35 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 index 30563222db743..ad6a6015f6de3 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 7bd93910cad21..5d1a2a2b6e81a 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-playerinspector--default--dark.png b/frontend/__snapshots__/components-playerinspector--default--dark.png index 3bd75598f3910..436a864b01e70 100644 Binary files a/frontend/__snapshots__/components-playerinspector--default--dark.png and b/frontend/__snapshots__/components-playerinspector--default--dark.png differ diff --git a/frontend/__snapshots__/components-playerinspector--default--light.png b/frontend/__snapshots__/components-playerinspector--default--light.png index 19dad22b1486b..037bdc7dac304 100644 Binary files a/frontend/__snapshots__/components-playerinspector--default--light.png and b/frontend/__snapshots__/components-playerinspector--default--light.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png index 30d180884665b..ebbdd65ee5643 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--light.png differ diff --git a/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPaneHeader.tsx b/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPaneHeader.tsx index 1346c218f556c..03f6a6e823984 100644 --- a/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPaneHeader.tsx +++ b/frontend/src/layout/navigation-3000/sidepanel/components/SidePanelPaneHeader.tsx @@ -1,5 +1,5 @@ import { IconX } from '@posthog/icons' -import { LemonButton, Tooltip } from '@posthog/lemon-ui' +import { LemonButton } from '@posthog/lemon-ui' import clsx from 'clsx' import { useActions, useValues } from 'kea' @@ -31,9 +31,13 @@ export function SidePanelPaneHeader({ children, title }: SidePanelPaneHeaderProp ) : null} {children} - - } onClick={() => closeSidePanel()} /> - + } + onClick={() => closeSidePanel()} + tooltip={modalMode ? 'Close' : 'Close this side panel'} + tooltipPlacement={modalMode ? 'top' : 'bottom-end'} + /> ) } diff --git a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx index 0eeada32885d2..b7cdf5940471e 100644 --- a/frontend/src/layout/navigation/TopBar/AccountPopover.tsx +++ b/frontend/src/layout/navigation/TopBar/AccountPopover.tsx @@ -17,7 +17,6 @@ import clsx from 'clsx' import { useActions, useValues } from 'kea' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture' -import { Tooltip } from 'lib/lemon-ui/Tooltip' import { UploadedLogo } from 'lib/lemon-ui/UploadedLogo' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { inviteLogic } from 'scenes/settings/organization/inviteLogic' @@ -85,27 +84,27 @@ function CurrentOrganization({ organization }: { organization: OrganizationBasic const { closeAccountPopover } = useActions(navigationLogic) return ( - - - } - sideIcon={} - fullWidth - to={urls.settings('organization')} - onClick={closeAccountPopover} - > -
- {organization.name} - -
-
-
+ + } + sideIcon={} + fullWidth + to={urls.settings('organization')} + onClick={closeAccountPopover} + tooltip="Organization settings" + tooltipPlacement="left" + > +
+ {organization.name} + +
+
) } diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index b443065a41e9d..16691c368dd0a 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -113,7 +113,11 @@ import { } from '~/types' import { AlertType, AlertTypeWrite } from './components/Alerts/types' -import { ErrorTrackingStackFrameContext } from './components/Errors/types' +import { + ErrorTrackingStackFrame, + ErrorTrackingStackFrameRecord, + ErrorTrackingSymbolSet, +} from './components/Errors/types' import { ACTIVITY_PAGE_SIZE, DashboardPrivilegeLevel, @@ -709,23 +713,33 @@ class ApiRequest { } public errorTrackingGroup(fingerprint: ErrorTrackingGroup['fingerprint'], teamId?: TeamType['id']): ApiRequest { - return this.errorTracking(teamId).addPathComponent(stringifiedFingerprint(fingerprint)) + return this.errorTracking(teamId) + .addPathComponent('group') + .addPathComponent(stringifiedFingerprint(fingerprint)) } - public errorTrackingMerge(fingerprint: ErrorTrackingGroup['fingerprint']): ApiRequest { + public errorTrackingGroupMerge(fingerprint: ErrorTrackingGroup['fingerprint']): ApiRequest { return this.errorTrackingGroup(fingerprint).addPathComponent('merge') } - public errorTrackingUploadSourceMaps(): ApiRequest { - return this.errorTracking().addPathComponent('upload_source_maps') + public errorTrackingSymbolSets(teamId?: TeamType['id']): ApiRequest { + return this.errorTracking(teamId).addPathComponent('symbol_sets') } - public errorTrackingStackFrames(): ApiRequest { - return this.errorTracking().addPathComponent('stack_frames') + public errorTrackingSymbolSet(id: ErrorTrackingSymbolSet['id']): ApiRequest { + return this.errorTrackingSymbolSets().addPathComponent(id) } - public errorTrackingStackFrameContexts(ids: string[]): ApiRequest { - return this.errorTrackingStackFrames().addPathComponent('contexts').withQueryString(toParams({ ids }, true)) + public errorTrackingStackFrames({ + raw_ids, + symbol_set, + }: { + raw_ids?: ErrorTrackingStackFrame['raw_id'][] + symbol_set?: ErrorTrackingSymbolSet['id'] + }): ApiRequest { + return this.errorTracking() + .addPathComponent('stack_frames') + .withQueryString(toParams({ raw_ids, symbol_set }, true)) } // # Warehouse @@ -1847,7 +1861,7 @@ const api = { }, errorTracking: { - async update( + async updateIssue( fingerprint: ErrorTrackingGroup['fingerprint'], data: Partial> ): Promise { @@ -1859,16 +1873,27 @@ const api = { mergingFingerprints: ErrorTrackingGroup['fingerprint'][] ): Promise<{ content: string }> { return await new ApiRequest() - .errorTrackingMerge(primaryFingerprint) + .errorTrackingGroup(primaryFingerprint) .create({ data: { merging_fingerprints: mergingFingerprints } }) }, + async updateSymbolSet(id: ErrorTrackingSymbolSet['id'], data: FormData): Promise { + return await new ApiRequest().errorTrackingSymbolSet(id).update({ data }) + }, + + async deleteSymbolSet(id: ErrorTrackingSymbolSet['id']): Promise { + return await new ApiRequest().errorTrackingSymbolSet(id).delete() + }, + + async symbolSets(): Promise<{ results: ErrorTrackingSymbolSet[] }> { + return await new ApiRequest().errorTrackingSymbolSets().get() + }, - async uploadSourceMaps(data: FormData): Promise<{ content: string }> { - return await new ApiRequest().errorTrackingUploadSourceMaps().create({ data }) + async symbolSetStackFrames(id: ErrorTrackingSymbolSet['id']): Promise { + return await new ApiRequest().errorTrackingStackFrames({ symbol_set: id }).get() }, - async fetchStackFrames(ids: string[]): Promise> { - return await new ApiRequest().errorTrackingStackFrameContexts(ids).get() + async stackFrames(raw_ids: ErrorTrackingStackFrame['raw_id'][]): Promise { + return await new ApiRequest().errorTrackingStackFrames({ raw_ids }).get() }, }, diff --git a/frontend/src/lib/components/Errors/ErrorDisplay.tsx b/frontend/src/lib/components/Errors/ErrorDisplay.tsx index 84cb12aedbd53..72e2acda24a36 100644 --- a/frontend/src/lib/components/Errors/ErrorDisplay.tsx +++ b/frontend/src/lib/components/Errors/ErrorDisplay.tsx @@ -1,7 +1,7 @@ import './ErrorDisplay.scss' import { IconFlag } from '@posthog/icons' -import { LemonBanner, LemonCollapse } from '@posthog/lemon-ui' +import { LemonBanner, LemonCollapse, Tooltip } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { TitledSnack } from 'lib/components/TitledSnack' import { LemonDivider } from 'lib/lemon-ui/LemonDivider' @@ -28,44 +28,58 @@ function StackTrace({ frames: ErrorTrackingStackFrame[] showAllFrames: boolean }): JSX.Element | null { - const { frameContexts } = useValues(stackFrameLogic) - const { loadFrameContexts } = useActions(stackFrameLogic) + const { stackFrameRecords } = useValues(stackFrameLogic) + const { loadFromRawIds } = useActions(stackFrameLogic) const displayFrames = showAllFrames ? frames : frames.filter((f) => f.in_app) useEffect(() => { - loadFrameContexts({ frames }) - }, [frames, loadFrameContexts]) + loadFromRawIds(frames.map(({ raw_id }) => raw_id)) + }, [frames, loadFromRawIds]) const initiallyActiveIndex = displayFrames.findIndex((f) => f.in_app) || 0 - const panels = displayFrames.map(({ raw_id, source, line, column, resolved_name, lang }, index) => { - const frameContext = frameContexts[raw_id] - return { - key: index, - header: ( -
- {source} - {resolved_name ? ( -
- in - {resolved_name} + 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}
- ) : null} - {line ? ( -
- @ - - {line} - {column && `:${column}`} - -
- ) : null} -
- ), - content: frameContext ? : null, - className: 'p-0', + {!resolved && ( +
+ + Unresolved + +
+ )} +
+ ), + content: + record && record.context ? ( + + ) : null, + className: 'p-0', + } } - }) + ) return } diff --git a/frontend/src/lib/components/Errors/stackFrameLogic.ts b/frontend/src/lib/components/Errors/stackFrameLogic.ts index 66d7ef0698bf9..12f7419384a38 100644 --- a/frontend/src/lib/components/Errors/stackFrameLogic.ts +++ b/frontend/src/lib/components/Errors/stackFrameLogic.ts @@ -1,23 +1,40 @@ -import { kea, path } from 'kea' +import { actions, kea, path } from 'kea' import { loaders } from 'kea-loaders' import api from 'lib/api' import type { stackFrameLogicType } from './stackFrameLogicType' -import { ErrorTrackingStackFrame, ErrorTrackingStackFrameContext } from './types' +import { ErrorTrackingStackFrame, ErrorTrackingStackFrameRecord, ErrorTrackingSymbolSet } from './types' + +export type KeyedStackFrameRecords = Record + +function mapStackFrameRecords( + newRecords: ErrorTrackingStackFrameRecord[], + initialRecords: KeyedStackFrameRecords +): KeyedStackFrameRecords { + return newRecords.reduce((frames, record) => ({ ...frames, [record.raw_id]: record }), initialRecords) +} export const stackFrameLogic = kea([ path(['components', 'Errors', 'stackFrameLogic']), + + actions({ + loadFromRawIds: (rawIds: ErrorTrackingStackFrame['raw_id'][]) => ({ rawIds }), + loadForSymbolSet: (symbolSetId: ErrorTrackingSymbolSet['id']) => ({ symbolSetId }), + }), + loaders(({ values }) => ({ - frameContexts: [ - {} as Record, + stackFrameRecords: [ + {} as KeyedStackFrameRecords, { - 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 } + loadFromRawIds: async ({ rawIds }) => { + const loadedRawIds = Object.keys(values.stackFrameRecords) + rawIds = rawIds.filter((rawId) => !loadedRawIds.includes(rawId)) + const response = await api.errorTracking.stackFrames(rawIds) + return mapStackFrameRecords(response, values.stackFrameRecords) + }, + loadForSymbolSet: async ({ symbolSetId }) => { + const response = await api.errorTracking.symbolSetStackFrames(symbolSetId) + return mapStackFrameRecords(response, values.stackFrameRecords) }, }, ], diff --git a/frontend/src/lib/components/Errors/types.ts b/frontend/src/lib/components/Errors/types.ts index 9a2cf59a662a3..3442e81884eb8 100644 --- a/frontend/src/lib/components/Errors/types.ts +++ b/frontend/src/lib/components/Errors/types.ts @@ -18,10 +18,10 @@ 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 + symbol_set_ref: ErrorTrackingSymbolSet['ref'] } export type ErrorTrackingStackFrameContext = { diff --git a/frontend/src/scenes/appScenes.ts b/frontend/src/scenes/appScenes.ts index 4a8e63759e124..911ade4320076 100644 --- a/frontend/src/scenes/appScenes.ts +++ b/frontend/src/scenes/appScenes.ts @@ -34,6 +34,7 @@ export const appScenes: Record any> = { [Scene.EarlyAccessFeatures]: () => import('./early-access-features/EarlyAccessFeatures'), [Scene.EarlyAccessFeature]: () => import('./early-access-features/EarlyAccessFeature'), [Scene.ErrorTracking]: () => import('./error-tracking/ErrorTrackingScene'), + [Scene.ErrorTrackingConfiguration]: () => import('./error-tracking/ErrorTrackingConfigurationScene'), [Scene.ErrorTrackingGroup]: () => import('./error-tracking/ErrorTrackingGroupScene'), [Scene.Surveys]: () => import('./surveys/Surveys'), [Scene.Survey]: () => import('./surveys/Survey'), diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingConfigurationScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingConfigurationScene.tsx new file mode 100644 index 0000000000000..b61056d229b43 --- /dev/null +++ b/frontend/src/scenes/error-tracking/ErrorTrackingConfigurationScene.tsx @@ -0,0 +1,128 @@ +import { IconTrash } from '@posthog/icons' +import { LemonButton, LemonCollapse, LemonTable, LemonTableColumns, LemonTabs } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { stackFrameLogic } from 'lib/components/Errors/stackFrameLogic' +import { ErrorTrackingSymbolSet } from 'lib/components/Errors/types' +import { JSONViewer } from 'lib/components/JSONViewer' +import { useEffect, useState } from 'react' +import { SceneExport } from 'scenes/sceneTypes' + +import { errorTrackingSymbolSetLogic } from './errorTrackingSymbolSetLogic' +import { SymbolSetUploadModal } from './SymbolSetUploadModal' + +export const scene: SceneExport = { + component: ErrorTrackingConfigurationScene, + logic: errorTrackingSymbolSetLogic, +} + +export function ErrorTrackingConfigurationScene(): JSX.Element { + const { missingSymbolSets, validSymbolSets } = useValues(errorTrackingSymbolSetLogic) + + return ( +
+

Symbol sets

+

+ Source maps are required to demangle any minified code in your exception stack traces. PostHog + automatically retrieves source maps where possible. Cases where it was not possible are listed below. + Source maps can be uploaded retroactively but changes will only apply to all future exceptions ingested. +

+ {missingSymbolSets.length > 0 && } + {validSymbolSets.length > 0 && } + +
+ ) +} + +const SymbolSetTable = ({ + dataSource, + pageSize, + missing, +}: { + dataSource: ErrorTrackingSymbolSet[] + pageSize: number + missing?: boolean +}): JSX.Element => { + const { symbolSetsLoading } = useValues(errorTrackingSymbolSetLogic) + const { deleteSymbolSet } = useActions(errorTrackingSymbolSetLogic) + + const columns: LemonTableColumns = [ + { title: missing && 'Missing symbol sets', dataIndex: 'ref' }, + { + dataIndex: 'id', + render: (_, { id }) => { + return ( +
+ {!missing && ( + } + onClick={() => deleteSymbolSet(id)} + className="py-1" + /> + )} +
+ ) + }, + }, + ] + + if (missing) { + columns.splice(1, 0, { title: 'Failure reason', dataIndex: 'failure_reason' }) + } + + return ( + + }, + }} + /> + ) +} + +const SymbolSetStackFrames = ({ symbolSet }: { symbolSet: ErrorTrackingSymbolSet }): JSX.Element => { + const { stackFrameRecords } = useValues(stackFrameLogic) + const { loadForSymbolSet } = useActions(stackFrameLogic) + const [activeTab, setActiveTab] = useState<'contents' | 'context'>('contents') + + useEffect(() => { + loadForSymbolSet(symbolSet.id) + }, [loadForSymbolSet, symbolSet]) + + const frames = Object.values(stackFrameRecords).filter((r) => r.symbol_set_ref == symbolSet.ref) + + return ( + ({ + key: id, + header: raw_id, + className: 'py-0', + content: ( + }, + context && { + key: 'context', + label: 'Context', + content: , + }, + ]} + /> + ), + }))} + embedded + /> + ) +} diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx index ae12520249a0d..3270a14d564cd 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx @@ -13,6 +13,7 @@ import { AssigneeSelect } from './AssigneeSelect' import ErrorTrackingFilters from './ErrorTrackingFilters' import { errorTrackingGroupSceneLogic } from './errorTrackingGroupSceneLogic' import { OverviewTab } from './groups/OverviewTab' +import { SymbolSetUploadModal } from './SymbolSetUploadModal' export const scene: SceneExport = { component: ErrorTrackingGroupScene, @@ -82,6 +83,7 @@ export function ErrorTrackingGroupScene(): JSX.Element { + ) } diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx index 60f9bedd45b37..54f05ae67caf2 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx @@ -1,20 +1,10 @@ import { TZLabel } from '@posthog/apps-common' import { IconGear } from '@posthog/icons' -import { - LemonButton, - LemonCheckbox, - LemonDivider, - LemonFileInput, - LemonModal, - LemonSegmentedButton, -} from '@posthog/lemon-ui' +import { LemonButton, LemonCheckbox, LemonDivider, LemonSegmentedButton } from '@posthog/lemon-ui' import clsx from 'clsx' import { BindLogic, useActions, useValues } from 'kea' -import { Form } from 'kea-forms' import { FeedbackNotice } from 'lib/components/FeedbackNotice' import { PageHeader } from 'lib/components/PageHeader' -import { IconUploadFile } from 'lib/lemon-ui/icons' -import { LemonField } from 'lib/lemon-ui/LemonField' import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' import { SceneExport } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' @@ -63,7 +53,6 @@ export function ErrorTrackingScene(): JSX.Element { return (
- @@ -180,61 +169,13 @@ const AssigneeColumn: QueryContextColumnComponent = (props) => { } const Header = (): JSX.Element => { - const { setIsConfigurationModalOpen } = useActions(errorTrackingSceneLogic) - return ( } onClick={() => setIsConfigurationModalOpen(true)}> + }> Configure } /> ) } - -const ConfigurationModal = (): JSX.Element => { - const { isConfigurationModalOpen, isUploadSourceMapSubmitting } = useValues(errorTrackingSceneLogic) - const { setIsConfigurationModalOpen } = useActions(errorTrackingSceneLogic) - - return ( - setIsConfigurationModalOpen(false)} - isOpen={isConfigurationModalOpen} - simple - > -
- -

Upload source map

-
- - - - - Add source map - -
- Drag and drop your local source map here or click to open the file browser. -
-
- } - /> - - - - setIsConfigurationModalOpen(false)}> - Cancel - - - Upload - - - - - ) -} diff --git a/frontend/src/scenes/error-tracking/SymbolSetUploadModal.tsx b/frontend/src/scenes/error-tracking/SymbolSetUploadModal.tsx new file mode 100644 index 0000000000000..28e7820759712 --- /dev/null +++ b/frontend/src/scenes/error-tracking/SymbolSetUploadModal.tsx @@ -0,0 +1,57 @@ +import { LemonButton, LemonFileInput, LemonModal } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { Form } from 'kea-forms' +import { IconUploadFile } from 'lib/lemon-ui/icons' +import { LemonField } from 'lib/lemon-ui/LemonField' + +import { errorTrackingSymbolSetLogic } from './errorTrackingSymbolSetLogic' + +export const SymbolSetUploadModal = (): JSX.Element => { + const { setUploadSymbolSetReference } = useActions(errorTrackingSymbolSetLogic) + const { uploadSymbolSetReference, isUploadSymbolSetSubmitting, uploadSymbolSet } = + useValues(errorTrackingSymbolSetLogic) + + const onClose = (): void => setUploadSymbolSetReference(null) + + return ( + +
+ +

Upload source map

+
+ + + + + Add source map + +
+ Drag and drop your local source map here or click to open the file browser. +
+ + } + /> +
+
+ + + Cancel + + + Upload + + +
+
+ ) +} diff --git a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx index 59a7c6bf52595..1b47979cb8b7a 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx +++ b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx @@ -63,7 +63,7 @@ export const errorTrackingDataNodeLogic = kea([ results.splice(recordIndex, 1, group) // optimistically update local results actions.setResponse({ ...response, results: results }) - await api.errorTracking.update(group.fingerprint, params) + await api.errorTracking.updateIssue(group.fingerprint, params) } }, })), diff --git a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts index 22d8f4c4692fb..506fcdec0e1e0 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts @@ -86,7 +86,7 @@ export const errorTrackingGroupSceneLogic = kea { - const response = await api.errorTracking.update(props.fingerprint, group) + const response = await api.errorTracking.updateIssue(props.fingerprint, group) return { ...values.group, ...response } }, }, diff --git a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts index c27b912ea2930..f2f2b8713fb4c 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts @@ -1,8 +1,5 @@ -import { lemonToast } from '@posthog/lemon-ui' import { actions, connect, kea, path, reducers, selectors } from 'kea' -import { forms } from 'kea-forms' import { subscriptions } from 'kea-subscriptions' -import api from 'lib/api' import { DataTableNode, ErrorTrackingQuery } from '~/queries/schema' @@ -30,9 +27,9 @@ export const errorTrackingSceneLogic = kea([ actions({ setOrder: (order: ErrorTrackingQuery['order']) => ({ order }), - setIsConfigurationModalOpen: (open: boolean) => ({ open }), setSelectedRowIndexes: (ids: number[]) => ({ ids }), }), + reducers({ order: [ 'last_seen' as ErrorTrackingQuery['order'], @@ -41,12 +38,6 @@ export const errorTrackingSceneLogic = kea([ setOrder: (_, { order }) => order, }, ], - isConfigurationModalOpen: [ - false as boolean, - { - setIsConfigurationModalOpen: (_, { open }) => open, - }, - ], selectedRowIndexes: [ [] as number[], { @@ -95,20 +86,4 @@ export const errorTrackingSceneLogic = kea([ subscriptions(({ actions }) => ({ query: () => actions.setSelectedRowIndexes([]), })), - - forms(({ actions }) => ({ - uploadSourceMap: { - defaults: { files: [] } as { files: File[] }, - submit: async ({ files }) => { - if (files.length > 0) { - const formData = new FormData() - const file = files[0] - formData.append('source_map', file) - await api.errorTracking.uploadSourceMaps(formData) - actions.setIsConfigurationModalOpen(false) - lemonToast.success('Source map uploaded') - } - }, - }, - })), ]) diff --git a/frontend/src/scenes/error-tracking/errorTrackingSymbolSetLogic.tsx b/frontend/src/scenes/error-tracking/errorTrackingSymbolSetLogic.tsx new file mode 100644 index 0000000000000..41f3a678bfb7f --- /dev/null +++ b/frontend/src/scenes/error-tracking/errorTrackingSymbolSetLogic.tsx @@ -0,0 +1,91 @@ +import { lemonToast } from '@posthog/lemon-ui' +import { actions, afterMount, kea, path, reducers, selectors } from 'kea' +import { forms } from 'kea-forms' +import { loaders } from 'kea-loaders' +import api from 'lib/api' +import { ErrorTrackingSymbolSet } from 'lib/components/Errors/types' +import { Scene } from 'scenes/sceneTypes' +import { urls } from 'scenes/urls' + +import { Breadcrumb } from '~/types' + +import type { errorTrackingSymbolSetLogicType } from './errorTrackingSymbolSetLogicType' + +export enum ErrorGroupTab { + Overview = 'overview', + Breakdowns = 'breakdowns', +} + +export const errorTrackingSymbolSetLogic = kea([ + path(['scenes', 'error-tracking', 'errorTrackingSymbolSetLogic']), + + actions({ + setUploadSymbolSetReference: (ref: ErrorTrackingSymbolSet['id'] | null) => ({ ref }), + }), + + reducers({ + uploadSymbolSetReference: [ + null as string | null, + { + setUploadSymbolSetReference: (_, { ref }) => ref, + }, + ], + }), + + loaders(({ values }) => ({ + symbolSets: [ + [] as ErrorTrackingSymbolSet[], + { + loadSymbolSets: async () => { + const response = await api.errorTracking.symbolSets() + return response.results + }, + deleteSymbolSet: async (ref) => { + await api.errorTracking.deleteSymbolSet(ref) + const newValues = [...values.symbolSets] + return newValues.filter((v) => v.ref !== ref) + }, + }, + ], + })), + + selectors({ + breadcrumbs: [ + () => [], + (): Breadcrumb[] => [ + { + key: Scene.ErrorTracking, + name: 'Error tracking', + path: urls.errorTracking(), + }, + { + key: Scene.ErrorTrackingConfiguration, + name: 'Configuration', + }, + ], + ], + validSymbolSets: [(s) => [s.symbolSets], (symbolSets) => symbolSets.filter((s) => !!s.storage_ptr)], + missingSymbolSets: [(s) => [s.symbolSets], (symbolSets) => symbolSets.filter((s) => !s.storage_ptr)], + }), + + forms(({ values, actions }) => ({ + uploadSymbolSet: { + defaults: { files: [] } as { files: File[] }, + submit: async ({ files }) => { + if (files.length > 0 && values.uploadSymbolSetReference) { + const formData = new FormData() + const file = files[0] + formData.append('source_map', file) + await api.errorTracking.updateSymbolSet(values.uploadSymbolSetReference, formData) + actions.setUploadSymbolSetReference(null) + actions.loadSymbolSets() + lemonToast.success('Source map uploaded') + } + }, + }, + })), + + afterMount(({ actions }) => { + actions.loadSymbolSets() + }), +]) diff --git a/frontend/src/scenes/sceneTypes.ts b/frontend/src/scenes/sceneTypes.ts index 2c0d227e5ba88..7b726aeb3249f 100644 --- a/frontend/src/scenes/sceneTypes.ts +++ b/frontend/src/scenes/sceneTypes.ts @@ -11,6 +11,7 @@ export enum Scene { ErrorProjectUnavailable = 'ProjectUnavailable', ErrorTracking = 'ErrorTracking', ErrorTrackingGroup = 'ErrorTrackingGroup', + ErrorTrackingConfiguration = 'ErrorTrackingConfiguration', Dashboards = 'Dashboards', Dashboard = 'Dashboard', Insight = 'Insight', diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 9f6309458496a..650ad53375b56 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -65,6 +65,10 @@ export const sceneConfigurations: Record = { projectBased: true, name: 'Error tracking', }, + [Scene.ErrorTrackingConfiguration]: { + projectBased: true, + name: 'Error tracking configuration', + }, [Scene.ErrorTrackingGroup]: { projectBased: true, name: 'Error tracking group', @@ -549,6 +553,7 @@ export const routes: Record = { [urls.earlyAccessFeatures()]: Scene.EarlyAccessFeatures, [urls.earlyAccessFeature(':id')]: Scene.EarlyAccessFeature, [urls.errorTracking()]: Scene.ErrorTracking, + [urls.errorTrackingConfiguration()]: Scene.ErrorTrackingConfiguration, [urls.errorTrackingGroup(':fingerprint')]: Scene.ErrorTrackingGroup, [urls.surveys()]: Scene.Surveys, [urls.survey(':id')]: Scene.Survey, diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index ccd4b7a482ff8..16b3146b7e1c7 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -163,6 +163,7 @@ export const urls = { /** @param id A UUID or 'new'. ':id' for routing. */ earlyAccessFeature: (id: string): string => `/early_access_features/${id}`, errorTracking: (): string => '/error_tracking', + errorTrackingConfiguration: (): string => '/error_tracking/configuration', errorTrackingGroup: (fingerprint: string): string => `/error_tracking/${fingerprint === ':fingerprint' ? fingerprint : encodeURIComponent(fingerprint)}`, surveys: (tab?: SurveysTabs): string => `/surveys${tab ? `?tab=${tab}` : ''}`, diff --git a/posthog/api/__init__.py b/posthog/api/__init__.py index 233a0df72703e..bf1db10b864a5 100644 --- a/posthog/api/__init__.py +++ b/posthog/api/__init__.py @@ -500,6 +500,13 @@ def register_grandfathered_environment_nested_viewset( ["project_id"], ) +projects_router.register( + r"error_tracking/symbol_sets", + error_tracking.ErrorTrackingSymbolSetViewSet, + "project_error_tracking_symbol_set", + ["team_id"], +) + # projects_router.register( # r"error_tracking", # error_tracking.ErrorTrackingGroupViewSet, diff --git a/posthog/api/error_tracking.py b/posthog/api/error_tracking.py index 8cb8d6d102148..f2e041656ae29 100644 --- a/posthog/api/error_tracking.py +++ b/posthog/api/error_tracking.py @@ -1,11 +1,18 @@ import structlog -from rest_framework import viewsets, response, serializers -from posthog.api.routing import TeamAndOrgViewSetMixin -from .forbid_destroy_model import ForbidDestroyModel +from rest_framework import serializers, viewsets, status +from rest_framework.response import Response +from rest_framework.exceptions import ValidationError + +from django.conf import settings -from posthog.api.utils import action +from posthog.api.forbid_destroy_model import ForbidDestroyModel +from posthog.api.routing import TeamAndOrgViewSetMixin +from posthog.models import ErrorTrackingSymbolSet from posthog.models.error_tracking import ErrorTrackingStackFrame +from posthog.models.utils import uuid7 +from posthog.storage import object_storage + FIFTY_MEGABYTES = 50 * 1024 * 1024 @@ -40,33 +47,13 @@ class ObjectStorageUnavailable(Exception): # 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.", -# ) - class ErrorTrackingStackFrameSerializer(serializers.ModelSerializer): + symbol_set_ref = serializers.CharField(source="symbol_set.ref") + class Meta: model = ErrorTrackingStackFrame - fields = ["raw_id", "context"] + fields = ["id", "raw_id", "created_at", "contents", "resolved", "context", "symbol_set_ref"] class ErrorTrackingStackFrameViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, viewsets.ReadOnlyModelViewSet): @@ -74,10 +61,63 @@ class ErrorTrackingStackFrameViewSet(TeamAndOrgViewSetMixin, ForbidDestroyModel, 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) + def safely_get_queryset(self, queryset): + if self.action == "list": + raw_ids = self.request.GET.getlist("raw_ids", []) + if raw_ids: + queryset = self.queryset.filter(raw_id__in=raw_ids) + + symbol_set = self.request.GET.get("symbol_set", None) + if symbol_set: + queryset = self.queryset.filter(symbol_set=symbol_set) + + return queryset.select_related("symbol_set").filter(team_id=self.team.id) + + +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, viewsets.ReadOnlyModelViewSet): + scope_object = "INTERNAL" + queryset = ErrorTrackingSymbolSet.objects.all() + serializer_class = ErrorTrackingSymbolSetSerializer + + def safely_get_queryset(self, queryset): + return queryset.filter(team_id=self.team.id) + + def destroy(self, request, *args, **kwargs): + symbol_set = self.get_object() + symbol_set.delete() + # TODO: delete file from s3 + return Response(status=status.HTTP_204_NO_CONTENT) + + def update(self, request, *args, **kwargs) -> Response: + symbol_set = self.get_object() + symbol_set.delete() + # TODO: delete file from s3 + storage_ptr = upload_symbol_set(request.FILES["source_map"], self.team_id) + symbol_set.storage_ptr = storage_ptr + symbol_set.save() + return Response({"ok": True}, status=status.HTTP_204_NO_CONTENT) + + +def upload_symbol_set(file, team_id) -> str: + try: + if settings.OBJECT_STORAGE_ENABLED: + 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}/{str(uuid7())}" + object_storage.write(upload_path, file) + return upload_path + else: + raise ObjectStorageUnavailable() + except ObjectStorageUnavailable: + raise ValidationError( + code="object_storage_required", + detail="Object storage must be available to allow source map uploads.", + ) diff --git a/posthog/api/test/test_error_tracking.py b/posthog/api/test/test_error_tracking.py index 8d48aff112960..29ef7342b6efa 100644 --- a/posthog/api/test/test_error_tracking.py +++ b/posthog/api/test/test_error_tracking.py @@ -2,9 +2,15 @@ import json from boto3 import resource +from rest_framework import status + from django.utils.http import urlsafe_base64_encode +from django.test import override_settings +from django.core.files.uploadedfile import SimpleUploadedFile + from posthog.test.base import APIBaseTest +from posthog.models import ErrorTrackingSymbolSet, ErrorTrackingStackFrame from botocore.config import Config from posthog.settings import ( OBJECT_STORAGE_ENDPOINT, @@ -88,41 +94,98 @@ def send_request(self, fingerprint, data, endpoint=""): # groups = ErrorTrackingGroup.objects.only("merged_fingerprints") # self.assertEqual(groups[0].merged_fingerprints, merging_fingerprints) - # def test_can_upload_a_source_map(self) -> None: - # with self.settings(OBJECT_STORAGE_ENABLED=True, OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER=TEST_BUCKET): - # with open(get_path_to("source.js.map"), "rb") as image: - # response = self.client.post( - # f"/api/projects/{self.team.id}/error_tracking/upload_source_maps", - # {"source_map": image}, - # format="multipart", - # ) - # self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json()) - - # def test_rejects_too_large_file_type(self) -> None: - # fifty_megabytes_plus_a_little = b"1" * (50 * 1024 * 1024 + 1) - # fake_big_file = SimpleUploadedFile( - # name="large_source.js.map", - # content=fifty_megabytes_plus_a_little, - # content_type="text/plain", - # ) - # response = self.client.post( - # f"/api/projects/{self.team.id}/error_tracking/upload_source_maps", - # {"source_map": fake_big_file}, - # format="multipart", - # ) - # self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json()) - # self.assertEqual(response.json()["detail"], "Source maps must be less than 50MB") - - # def test_rejects_upload_when_object_storage_is_unavailable(self) -> None: - # with override_settings(OBJECT_STORAGE_ENABLED=False): - # fake_big_file = SimpleUploadedFile(name="large_source.js.map", content=b"", content_type="text/plain") - # response = self.client.post( - # f"/api/projects/{self.team.id}/error_tracking/upload_source_maps", - # {"source_map": fake_big_file}, - # format="multipart", - # ) - # self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json()) - # self.assertEqual( - # response.json()["detail"], - # "Object storage must be available to allow source map uploads.", - # ) + def test_can_upload_a_source_map(self) -> None: + with self.settings(OBJECT_STORAGE_ENABLED=True, OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER=TEST_BUCKET): + symbol_set = ErrorTrackingSymbolSet.objects.create( + ref="https://app-static-prod.posthog.com/static/chunk-BPTF6YBO.js", team=self.team, storage_ptr=None + ) + + with open(get_path_to("source.js.map"), "rb") as image: + response = self.client.put( + f"/api/projects/{self.team.id}/error_tracking/symbol_sets/{symbol_set.id}", + {"source_map": image}, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_rejects_too_large_file_type(self) -> None: + symbol_set = ErrorTrackingSymbolSet.objects.create( + ref="https://app-static-prod.posthog.com/static/chunk-BPTF6YBO.js", team=self.team, storage_ptr=None + ) + fifty_megabytes_plus_a_little = b"1" * (50 * 1024 * 1024 + 1) + fake_big_file = SimpleUploadedFile( + name="large_source.js.map", + content=fifty_megabytes_plus_a_little, + content_type="text/plain", + ) + response = self.client.put( + f"/api/projects/{self.team.id}/error_tracking/symbol_sets/{symbol_set.id}", + {"source_map": fake_big_file}, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json()) + self.assertEqual(response.json()["detail"], "Source maps must be less than 50MB") + + def test_rejects_upload_when_object_storage_is_unavailable(self) -> None: + symbol_set = ErrorTrackingSymbolSet.objects.create( + ref="https://app-static-prod.posthog.com/static/chunk-BPTF6YBO.js", team=self.team, storage_ptr=None + ) + with override_settings(OBJECT_STORAGE_ENABLED=False): + fake_big_file = SimpleUploadedFile(name="large_source.js.map", content=b"", content_type="text/plain") + response = self.client.put( + f"/api/projects/{self.team.id}/error_tracking/symbol_sets/{symbol_set.id}", + {"source_map": fake_big_file}, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, response.json()) + self.assertEqual( + response.json()["detail"], + "Object storage must be available to allow source map uploads.", + ) + + def test_fetching_symbol_sets(self): + other_team = self.create_team_with_organization(organization=self.organization) + ErrorTrackingSymbolSet.objects.create(ref="source_1", team=self.team, storage_ptr=None) + ErrorTrackingSymbolSet.objects.create( + ref="source_2", team=self.team, storage_ptr="https://app-static-prod.posthog.com/static/chunk-BPTF6YBO.js" + ) + ErrorTrackingSymbolSet.objects.create( + ref="source_2", team=other_team, storage_ptr="https://app-static-prod.posthog.com/static/chunk-BPTF6YBO.js" + ) + + self.assertEqual(ErrorTrackingSymbolSet.objects.count(), 3) + + # it only fetches symbol sets for the specified team + response = self.client.get(f"/api/projects/{self.team.id}/error_tracking/symbol_sets") + self.assertEqual(len(response.json()["results"]), 2) + + def test_fetching_stack_frames(self): + other_team = self.create_team_with_organization(organization=self.organization) + symbol_set = ErrorTrackingSymbolSet.objects.create(ref="source_1", team=self.team, storage_ptr=None) + other_symbol_set = ErrorTrackingSymbolSet.objects.create(ref="source_2", team=self.team, storage_ptr=None) + ErrorTrackingStackFrame.objects.create( + raw_id="raw_id", team=self.team, symbol_set=symbol_set, resolved=True, contents={} + ) + ErrorTrackingStackFrame.objects.create( + raw_id="other_raw_id", team=self.team, symbol_set=other_symbol_set, resolved=True, contents={} + ) + ErrorTrackingStackFrame.objects.create( + raw_id="raw_id", team=other_team, symbol_set=symbol_set, resolved=True, contents={} + ) + + self.assertEqual(ErrorTrackingStackFrame.objects.count(), 3) + + # it only fetches stack traces for the specified team + response = self.client.get(f"/api/projects/{self.team.id}/error_tracking/stack_frames") + self.assertEqual(len(response.json()["results"]), 2) + + # fetching can be filtered by raw_ids + response = self.client.get(f"/api/projects/{self.team.id}/error_tracking/stack_frames?raw_ids=raw_id") + self.assertEqual(len(response.json()["results"]), 1) + + # fetching can be filtered by symbol set + response = self.client.get( + f"/api/projects/{self.team.id}/error_tracking/stack_frames?symbol_set={symbol_set.id}" + ) + self.assertEqual(len(response.json()["results"]), 1) + self.assertEqual(response.json()["results"][0]["symbol_set_ref"], symbol_set.ref) diff --git a/posthog/models/__init__.py b/posthog/models/__init__.py index 90579100c6c10..d51a87e05eb75 100644 --- a/posthog/models/__init__.py +++ b/posthog/models/__init__.py @@ -30,7 +30,7 @@ from .element import Element from .element_group import ElementGroup from .entity import Entity -from .error_tracking import ErrorTrackingGroup +from .error_tracking import ErrorTrackingGroup, ErrorTrackingStackFrame, ErrorTrackingSymbolSet from .event.event import Event from .event_buffer import EventBuffer from .event_definition import EventDefinition @@ -102,6 +102,8 @@ "ElementGroup", "Entity", "ErrorTrackingGroup", + "ErrorTrackingStackFrame", + "ErrorTrackingSymbolSet", "Event", "EventBuffer", "EventDefinition", @@ -119,6 +121,7 @@ "InsightViewed", "InstanceSetting", "Integration", + "InviteExpiredException", "MessagingRecord", "Notebook", "MigrationStatus", diff --git a/posthog/settings/object_storage.py b/posthog/settings/object_storage.py index d8e67e3ad2f7c..147fe5e18ca64 100644 --- a/posthog/settings/object_storage.py +++ b/posthog/settings/object_storage.py @@ -31,5 +31,5 @@ OBJECT_STORAGE_EXPORTS_FOLDER = os.getenv("OBJECT_STORAGE_EXPORTS_FOLDER", "exports") OBJECT_STORAGE_MEDIA_UPLOADS_FOLDER = os.getenv("OBJECT_STORAGE_MEDIA_UPLOADS_FOLDER", "media_uploads") OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER = os.getenv( - "OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER", "error_tracking_source_maps" + "OBJECT_STORAGE_ERROR_TRACKING_SOURCE_MAPS_FOLDER", "symbolsets" )