Skip to content

Commit

Permalink
feat(frontend): link to crash/anr details from session replay
Browse files Browse the repository at this point in the history
closes #1251
  • Loading branch information
anupcowkur committed Sep 17, 2024
1 parent add81df commit c477ef7
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 12 deletions.
18 changes: 16 additions & 2 deletions backend/api/measure/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4722,14 +4722,28 @@ func GetSession(c *gin.Context) {

exceptionEvents := eventMap[event.TypeException]
if len(exceptionEvents) > 0 {
exceptions := replay.ComputeExceptions(exceptionEvents)
exceptions, err := replay.ComputeExceptions(c, app.ID, exceptionEvents)
if err != nil {
msg := fmt.Sprintf(`unable to compute exceptions for session %q for app %q`, sessionId, app.ID)
c.JSON(http.StatusNotFound, gin.H{
"error": msg,
})
return
}
threadedExceptions := replay.GroupByThreads(exceptions)
threads.Organize(event.TypeException, threadedExceptions)
}

anrEvents := eventMap[event.TypeANR]
if len(anrEvents) > 0 {
anrs := replay.ComputeANRs(anrEvents)
anrs, err := replay.ComputeANRs(c, app.ID, anrEvents)
if err != nil {
msg := fmt.Sprintf(`unable to compute ANRs for session %q for app %q`, sessionId, app.ID)
c.JSON(http.StatusNotFound, gin.H{
"error": msg,
})
return
}
threadedANRs := replay.GroupByThreads(anrs)
threads.Organize(event.TypeANR, threadedANRs)
}
Expand Down
57 changes: 53 additions & 4 deletions backend/api/replay/critical.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ package replay

import (
"backend/api/event"
"backend/api/server"
"context"
"time"

"github.com/google/uuid"
"github.com/leporo/sqlf"
)

// Exception represents exception events suitable
// for session replay.
type Exception struct {
EventType string `json:"event_type"`
UserTriggered bool `json:"user_triggered"`
GroupId *uuid.UUID `json:"group_id"`
Type string `json:"type"`
Message string `json:"message"`
MethodName string `json:"method_name"`
Expand Down Expand Up @@ -39,6 +45,7 @@ func (e Exception) GetTimestamp() time.Time {
// for session replay.
type ANR struct {
EventType string `json:"event_type"`
GroupId *uuid.UUID `json:"group_id"`
Type string `json:"type"`
Message string `json:"message"`
MethodName string `json:"method_name"`
Expand All @@ -65,11 +72,33 @@ func (a ANR) GetTimestamp() time.Time {

// ComputeExceptions computes exceptions
// for session replay.
func ComputeExceptions(events []event.EventField) (result []ThreadGrouper) {
func ComputeExceptions(ctx context.Context, appId *uuid.UUID, events []event.EventField) (result []ThreadGrouper, err error) {
for _, event := range events {

var groupId *uuid.UUID

if !event.Exception.Handled {
stmt := sqlf.PostgreSQL.
From("public.unhandled_exception_groups").
Select("id").
Where("app_id = ?", appId).
Where("fingerprint = ?", event.Exception.Fingerprint)

defer stmt.Close()

row := server.Server.PgPool.QueryRow(ctx, stmt.String(), stmt.Args()...)

err := row.Scan(&groupId)

if err != nil {
return nil, err
}
}

exceptions := Exception{
event.Type,
event.UserTriggered,
groupId,
event.Exception.GetType(),
event.Exception.GetMessage(),
event.Exception.GetMethodName(),
Expand All @@ -85,15 +114,35 @@ func ComputeExceptions(events []event.EventField) (result []ThreadGrouper) {
result = append(result, exceptions)
}

return
return result, nil
}

// ComputeANR computes anrs
// for session replay.
func ComputeANRs(events []event.EventField) (result []ThreadGrouper) {
func ComputeANRs(ctx context.Context, appId *uuid.UUID, events []event.EventField) (result []ThreadGrouper, err error) {
for _, event := range events {

var groupId *uuid.UUID

stmt := sqlf.PostgreSQL.
From("public.anr_groups").
Select("id").
Where("app_id = ?", appId).
Where("fingerprint = ?", event.ANR.Fingerprint)

defer stmt.Close()

row := server.Server.PgPool.QueryRow(ctx, stmt.String(), stmt.Args()...)

err := row.Scan(&groupId)

if err != nil {
return nil, err
}

anrs := ANR{
event.Type,
groupId,
event.ANR.GetType(),
event.ANR.GetMessage(),
event.ANR.GetMethodName(),
Expand All @@ -108,5 +157,5 @@ func ComputeANRs(events []event.EventField) (result []ThreadGrouper) {
result = append(result, anrs)
}

return
return result, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { formatMillisToHumanReadable } from "@/app/utils/time_utils";
import { useRouter } from 'next/navigation';
import { useEffect, useState } from "react";

export default function Session({ params }: { params: { appId: string, sessionId: string } }) {
export default function Session({ params }: { params: { teamId: string, appId: string, sessionId: string } }) {
const router = useRouter()

const [sessionReplay, setSessionReplay] = useState(emptySessionReplay);
Expand Down Expand Up @@ -50,7 +50,7 @@ export default function Session({ params }: { params: { appId: string, sessionId
<p className="font-sans"> App version: {sessionReplay.attribute.app_version}</p>
<p className="font-sans"> Network type: {sessionReplay.attribute.network_type}</p>
<div className="py-6" />
<SessionReplay sessionReplay={sessionReplay} />
<SessionReplay teamId={params.teamId} appId={params.appId} sessionReplay={sessionReplay} />
</div>}
</div>

Expand Down
6 changes: 4 additions & 2 deletions frontend/dashboard/app/components/session_replay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import DropdownSelect, { DropdownSelectType } from './dropdown_select'
import { DateTime } from 'luxon'

interface SessionReplayProps {
teamId: string
appId: string
sessionReplay: typeof emptySessionReplay
}

const SessionReplay: React.FC<SessionReplayProps> = ({ sessionReplay }) => {
const SessionReplay: React.FC<SessionReplayProps> = ({ teamId, appId, sessionReplay }) => {

const cpuData = sessionReplay.cpu_usage != null ? [
{
Expand Down Expand Up @@ -302,7 +304,7 @@ const SessionReplay: React.FC<SessionReplayProps> = ({ sessionReplay }) => {
<SessionReplayEventVerticalConnector milliseconds={DateTime.fromISO(e.timestamp, { zone: 'utc' }).toMillis() - DateTime.fromISO(events[index - 1].timestamp, { zone: 'utc' }).toMillis()} />
}
{index > 0 && <div className='py-2' />}
<SessionReplayEventAccordion eventType={e.eventType} eventDetails={e.details} timestamp={e.timestamp} threadName={e.thread} id={`${e.eventType}-${index}`} active={false} />
<SessionReplayEventAccordion teamId={teamId} appId={appId} eventType={e.eventType} eventDetails={e.details} timestamp={e.timestamp} threadName={e.thread} id={`${e.eventType}-${index}`} active={false} />
</div>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import FilterPill from './filter_pill'
import { formatDateToHumanReadableDateTime } from '../utils/time_utils'
import { formatToCamelCase } from '../utils/string_utils'
import Image from 'next/image';
import Link from 'next/link'

type SessionReplayEventAccordionpProps = {
teamId: string
appId: string
eventType: string
eventDetails: any
threadName: string
Expand All @@ -16,6 +19,8 @@ type SessionReplayEventAccordionpProps = {
}

export default function SessionReplayEventAccordion({
teamId,
appId,
eventType,
eventDetails,
threadName,
Expand Down Expand Up @@ -131,7 +136,7 @@ export default function SessionReplayEventAccordion({
} else {
return `${key}: ${getBodyFromEventDetails(value)}`;
}
} else if (value === '') {
} else if (value === '' || value === null) {
return `${key}: --`;
} else if (key === 'stacktrace') {
return `${key}: \n\t${(value as string).replace(/\n/g, '\n\t')}`;
Expand All @@ -146,7 +151,7 @@ export default function SessionReplayEventAccordion({
// Return screenshots for exceptions
if (eventType === 'exception' || eventType === 'anr') {
return (
<div className='flex flex-wrap gap-8 p-4 items-center'>
<div className='flex flex-wrap gap-8 px-4 pt-4 items-center'>
{eventDetails.attachments.map((attachment: {
key: string, location: string
}, index: number) => (
Expand All @@ -165,6 +170,16 @@ export default function SessionReplayEventAccordion({
}
}

function getExceptionOverviewLinkFromEventDetails(): ReactNode {
if ((eventType === "exception" && eventDetails.user_triggered === false) || eventType === "anr") {
return (
<div className='px-4 pt-4'>
<Link key={eventDetails.id} href={`/${teamId}/${eventType === "exception" ? 'crashes' : 'anrs'}/${appId}/${eventDetails.group_id}/${eventDetails.type + "@" + eventDetails.file_name}`} className="outline-none justify-center w-fit hover:bg-yellow-200 active:bg-yellow-300 focus-visible:bg-yellow-200 border border-white hover:border-black rounded-md text-white hover:text-black font-display transition-colors duration-100 py-2 px-4">View {eventType === "exception" ? 'Crash' : 'ANR'} Details</Link>
</div>
)
}
}

return (
<div className={`border border-black rounded-md`}>
<button className={`w-full p-4 outline-none rounded-t-md ${!accordionOpen ? 'rounded-b-md' : ''} font-display ${getColorFromEventType()} `}
Expand All @@ -188,6 +203,7 @@ export default function SessionReplayEventAccordion({
>
<div className="overflow-hidden flex flex-col">
{getAttachmentsFromEventDetails()}
{getExceptionOverviewLinkFromEventDetails()}
<p className="whitespace-pre-wrap p-4 text-white">
{getBodyFromEventDetails(eventDetails)}
</p>
Expand Down

0 comments on commit c477ef7

Please sign in to comment.