diff --git a/backend/api/event/event.go b/backend/api/event/event.go index 2334ac137..e962dcdaf 100644 --- a/backend/api/event/event.go +++ b/backend/api/event/event.go @@ -4,6 +4,7 @@ import ( "backend/api/platform" "crypto/md5" "encoding/hex" + "errors" "fmt" "net" "regexp" @@ -246,17 +247,49 @@ func makeTitle(t, m string) (typeMessage string) { return } +// ExceptionUnitiOS represents iOS specific +// structure to work with iOS exceptions. +type ExceptionUnitiOS struct { + // Signal is the BSD termination signal. + Signal string `json:"signal" binding:"required"` + // ThreadName is the name of the thread. + ThreadName string `json:"thread_name" binding:"required"` + // ThreadSequence is the order of the thread + // in the iOS exception. + ThreadSequence uint `json:"thread_sequence" binding:"required"` + // OSBuildNumber is the operating system's + // build number. + OSBuildNumber string `json:"os_build_number" binding:"required"` +} + +// ExceptionUnit represents a cross-platform +// structure to work with parts of an exception. type ExceptionUnit struct { - Type string `json:"type" binding:"required"` + // Type is the type of the exception. + Type string `json:"type" binding:"required"` + // Message is the exception's message. Message string `json:"message"` - Frames Frames `json:"frames" binding:"required"` + // Frames is a collection of exception's frames. + Frames Frames `json:"frames" binding:"required"` + ExceptionUnitiOS } type ExceptionUnits []ExceptionUnit +// ThreadiOS represents iOS specific structure +// to work with iOS exceptions. +type ThreadiOS struct { + Sequence uint `json:"sequence"` +} + +// Thread represents a cross-platform +// structure to work with exception threads. type Thread struct { - Name string `json:"name" binding:"required"` + // Name is the name of the thread. + Name string `json:"name" binding:"required"` + // Frames is the collection of stackframe objects. Frames Frames `json:"frames" binding:"required"` + ThreadiOS } type Threads []Thread @@ -277,6 +310,13 @@ type Exception struct { Foreground bool `json:"foreground" binding:"required"` } +// FingerprintComputer describes the behavior +// to compute a unique fingerprint of any +// underlying structure. +type FingerprintComputer interface { + ComputeFingerprint() error +} + type AppExit struct { Reason string `json:"reason" binding:"required"` Importance string `json:"importance" binding:"required"` @@ -1097,6 +1137,28 @@ func (e *EventField) Validate() error { return nil } +// GetPlatform determines the exception belongs +// to which platform. +func (e Exception) GetPlatform() (p string) { + p = platform.Unknown + if len(e.Exceptions) < 1 || len(e.Threads) < 1 { + return p + } + + // Might be possible to detect the platform + // in a more robust manner + // + // FIXME: Revisit the heuristics for platform + // determination + if e.Exceptions[0].Signal != "" && e.Threads[0].Sequence != 0 { + p = platform.IOS + } else { + p = platform.Android + } + + return +} + // IsNested returns true in case of // multiple nested exceptions. func (e Exception) IsNested() bool { @@ -1208,40 +1270,72 @@ func (e Exception) Stacktrace() string { return b.String() } -// ComputeExceptionFingerprint computes a fingerprint -// from the exception data. -func (e *Exception) ComputeExceptionFingerprint() (err error) { +// ComputeFingerprint computes a fingerprint +// for the exception. +func (e *Exception) ComputeFingerprint() (err error) { if len(e.Exceptions) == 0 { - return fmt.Errorf("error computing exception fingerprint: no exceptions found") + return errors.New("error computing exception fingerprint: no exceptions found") } - // Get the innermost exception - innermostException := e.Exceptions[len(e.Exceptions)-1] + // input holds the raw input to + // compute the fingerprint + input := "" - // Get the exception type - exceptionType := innermostException.Type + // sep is the separator to separate + // parts of the input + sep := ":" - // Initialize fingerprint data with the exception type - fingerprintData := exceptionType + switch e.GetPlatform() { + case platform.Android: + // get the innermost exception + innermostException := e.Exceptions[len(e.Exceptions)-1] - // Get the method name and file name from the first frame of the innermost exception - if len(innermostException.Frames) > 0 { - methodName := innermostException.Frames[0].MethodName - fileName := innermostException.Frames[0].FileName + // initialize fingerprint data with the exception type + input = innermostException.Type + + // get the method name and file name from the first frame of the innermost exception + if len(innermostException.Frames) > 0 { + methodName := innermostException.Frames[0].MethodName + fileName := innermostException.Frames[0].FileName + + // Include any non-empty information + if methodName != "" { + input += sep + methodName + } + if fileName != "" { + input += sep + fileName + } + } + case platform.IOS: + // get the first exception unit + // FIXME: might need to use ThreadSequence here + firstUnit := e.Exceptions[0] + + input = firstUnit.Type + + if len(firstUnit.Frames) < 1 { + break + } + + methodName := firstUnit.Frames[0].MethodName + fileName := firstUnit.Frames[0].FileName - // Include any non-empty information if methodName != "" { - fingerprintData += ":" + methodName + input += sep + methodName } if fileName != "" { - fingerprintData += ":" + fileName + input += sep + fileName } + default: + return errors.New("failed to compute fingerprint for unknown platform") } // Compute the fingerprint - e.Fingerprint = computeFingerprint(fingerprintData) + // e.Fingerprint = computeFingerprint(input) + hash := md5.Sum([]byte(input)) + e.Fingerprint = hex.EncodeToString(hash[:]) - return nil + return } // IsNested returns true in case of @@ -1352,9 +1446,9 @@ func (a ANR) Stacktrace() string { return b.String() } -// ComputeANRFingerprint computes a fingerprint +// ComputeFingerprint computes a fingerprint // from the ANR data. -func (a *ANR) ComputeANRFingerprint() (err error) { +func (a *ANR) ComputeFingerprint() (err error) { if len(a.Exceptions) == 0 { return fmt.Errorf("error computing ANR fingerprint: no exceptions found") } @@ -1383,12 +1477,8 @@ func (a *ANR) ComputeANRFingerprint() (err error) { } // Compute the fingerprint - a.Fingerprint = computeFingerprint(fingerprintData) + hash := md5.Sum([]byte(fingerprintData)) + a.Fingerprint = hex.EncodeToString(hash[:]) return nil } - -func computeFingerprint(data string) string { - hash := md5.Sum([]byte(data)) - return hex.EncodeToString(hash[:]) -} diff --git a/backend/api/event/frame.go b/backend/api/event/frame.go index 6fe6ad2ee..57da3b647 100644 --- a/backend/api/event/frame.go +++ b/backend/api/event/frame.go @@ -14,13 +14,36 @@ const FramePrefix = "\tat " // that appears in Android stacktraces. const GenericPrefix = ": " +type FrameiOS struct { + // FrameIndex is the sequence of the frame. + FrameIndex int `json:"frame_index"` + // BinaryName is the name of the iOS binary image. + BinaryName string `json:"binary_name"` + // BinaryAddress is the binary load address. + BinaryAddress string `json:"binary_address"` + // SymbolAddress is the address to symbolicate. + SymbolAddress string `json:"symbol_address"` + // Offset is the byte offset. + Offset int `json:"offset"` + // InApp is `true` if the frame originates + // from the app module. + InApp bool `json:"in_app"` +} + type Frame struct { - LineNum int `json:"line_num"` - ColNum int `json:"col_num"` + // LineNum is the line number of the method. + LineNum int `json:"line_num"` + // ColNum is the column number of the method. + ColNum int `json:"col_num"` + // ModuleName is the name of the originating module. ModuleName string `json:"module_name"` - FileName string `json:"file_name"` - ClassName string `json:"class_name"` + // FileName is the name of the originating file. + FileName string `json:"file_name"` + // ClassName is the name of the originating class. + ClassName string `json:"class_name"` + // MethodName is the name of the originating method. MethodName string `json:"method_name"` + FrameiOS } type Frames []Frame diff --git a/backend/api/journey/android.go b/backend/api/journey/android.go index cc379cfbe..00ef7daaa 100644 --- a/backend/api/journey/android.go +++ b/backend/api/journey/android.go @@ -519,7 +519,7 @@ func NewJourneyAndroid(events []event.EventField, opts *Options) (journey *Journ } else if issue { // find the previous activity node - // and attach the issue to the node. + // and attach the issue to that node. c := i for { c-- diff --git a/backend/api/journey/ios.go b/backend/api/journey/ios.go index 686007367..4bc2c2fc4 100644 --- a/backend/api/journey/ios.go +++ b/backend/api/journey/ios.go @@ -370,7 +370,7 @@ func NewJourneyiOS(events []event.EventField, opts *Options) (journey *JourneyiO node.IsSwiftUI = true } else if issue { // find the previous view node and - // attach the issue to the node. + // attach the issue to that node. c := i for { c-- @@ -381,7 +381,7 @@ func NewJourneyiOS(events []event.EventField, opts *Options) (journey *JourneyiO } // we only add issues to view nodes - if journey.Nodes[i].IsViewController || journey.Nodes[i].IsSwiftUI { + if journey.Nodes[c].IsViewController || journey.Nodes[c].IsSwiftUI { addIssue := false // only add exception if requested and if the issue exists diff --git a/backend/api/measure/app.go b/backend/api/measure/app.go index c85feaa6a..7d5650925 100644 --- a/backend/api/measure/app.go +++ b/backend/api/measure/app.go @@ -17,6 +17,7 @@ import ( "backend/api/group" "backend/api/journey" "backend/api/metrics" + "backend/api/numeric" "backend/api/paginate" "backend/api/platform" "backend/api/server" @@ -690,7 +691,7 @@ func (a App) GetSizeMetrics(ctx context.Context, af *filter.AppFilter, versions func (a App) GetIssueFreeMetrics( ctx context.Context, af *filter.AppFilter, - versions filter.Versions, + unselectedVersions filter.Versions, ) ( crashFree *metrics.CrashFreeSession, perceivedCrashFree *metrics.PerceivedCrashFreeSession, @@ -775,13 +776,13 @@ func (a App) GetIssueFreeMetrics( perceivedANRFree.ANRFreeSessions = math.NaN() } } else { - crashFree.CrashFreeSessions = math.Round(1-float64(crashSelected/selected)) * 100 - perceivedCrashFree.CrashFreeSessions = math.Round(1-float64(perceivedCrashSelected/selected)) * 100 + crashFree.CrashFreeSessions = numeric.RoundTwoDecimalsFloat64((1 - (float64(crashSelected) / float64(selected))) * 100) + perceivedCrashFree.CrashFreeSessions = numeric.RoundTwoDecimalsFloat64((1 - (float64(perceivedCrashSelected) / float64(selected))) * 100) switch a.Platform { case platform.Android: - anrFree.ANRFreeSessions = math.Round(1-float64(anrSelected/selected)) * 100 - perceivedANRFree.ANRFreeSessions = math.Round(1-float64(perceivedANRSelected/selected)) * 100 + anrFree.ANRFreeSessions = numeric.RoundTwoDecimalsFloat64((1 - (float64(anrSelected) / float64(selected))) * 100) + perceivedANRFree.ANRFreeSessions = numeric.RoundTwoDecimalsFloat64((1 - (float64(perceivedANRSelected) / float64(selected))) * 100) } } @@ -795,28 +796,28 @@ func (a App) GetIssueFreeMetrics( perceivedANRFreeUnselected = math.NaN() } } else { - crashFreeUnselected = math.Round(1-float64(crashUnselected/unselected)) * 100 - perceivedCrashFreeUnselected = math.Round(1-float64(perceivedCrashUnselected/unselected)) * 100 + crashFreeUnselected = numeric.RoundTwoDecimalsFloat64((1 - (float64(crashUnselected) / float64(unselected))) * 100) + perceivedCrashFreeUnselected = numeric.RoundTwoDecimalsFloat64((1 - (float64(perceivedCrashUnselected) / float64(unselected))) * 100) switch a.Platform { case platform.Android: - anrFreeUnselected = math.Round(1-float64(anrUnselected/unselected)) * 100 - perceivedANRFreeUnselected = math.Round(1-float64(perceivedANRUnselected/unselected)) * 100 + anrFreeUnselected = numeric.RoundTwoDecimalsFloat64((1 - (float64(anrUnselected) / float64(unselected))) * 100) + perceivedANRFreeUnselected = numeric.RoundTwoDecimalsFloat64((1 - (float64(perceivedANRUnselected) / float64(unselected))) * 100) } } // compute delta - if versions.HasVersions() { + if unselectedVersions.HasVersions() { // avoid division by zero if crashFreeUnselected != 0 { // Round to two decimal places - crashFree.Delta = math.Round(crashFree.CrashFreeSessions/crashFreeUnselected*100) / 100 + crashFree.Delta = numeric.RoundTwoDecimalsFloat64(crashFree.CrashFreeSessions / crashFreeUnselected) } else { crashFree.Delta = 1 } if perceivedCrashFreeUnselected != 0 { - crashFree.Delta = math.Round(perceivedCrashFree.CrashFreeSessions/perceivedCrashFreeUnselected*100) / 100 + crashFree.Delta = numeric.RoundTwoDecimalsFloat64(perceivedCrashFree.CrashFreeSessions / perceivedCrashFreeUnselected) } else { perceivedCrashFree.Delta = 1 } @@ -824,13 +825,13 @@ func (a App) GetIssueFreeMetrics( switch a.Platform { case platform.Android: if anrFreeUnselected != 0 { - anrFree.Delta = math.Round(anrFree.ANRFreeSessions/anrFreeUnselected*100) / 100 + anrFree.Delta = numeric.RoundTwoDecimalsFloat64(anrFree.ANRFreeSessions / anrFreeUnselected) } else { anrFree.Delta = 1 } if perceivedANRFreeUnselected != 0 { - perceivedANRFree.Delta = math.Round(perceivedANRFree.ANRFreeSessions/perceivedANRFreeUnselected*100) / 100 + perceivedANRFree.Delta = numeric.RoundTwoDecimalsFloat64(perceivedANRFree.ANRFreeSessions / perceivedANRFreeUnselected) } else { perceivedANRFree.Delta = 1 } @@ -1446,6 +1447,11 @@ func (a *App) GetSessionEvents(ctx context.Context, sessionId uuid.UUID) (*Sessi `gesture_scroll.end_x`, `gesture_scroll.end_y`, `toString(gesture_scroll.direction)`, + `exception.handled`, + `exception.fingerprint`, + `exception.foreground`, + `exception.exceptions`, + `exception.threads`, `toString(lifecycle_app.type)`, `cold_launch.process_start_uptime`, `cold_launch.process_start_requested_uptime`, @@ -1509,11 +1515,6 @@ func (a *App) GetSessionEvents(ctx context.Context, sessionId uuid.UUID) (*Sessi `anr.foreground`, `anr.exceptions`, `anr.threads`, - `exception.handled`, - `exception.fingerprint`, - `exception.foreground`, - `exception.exceptions`, - `exception.threads`, `toString(app_exit.reason)`, `toString(app_exit.importance)`, `app_exit.trace`, @@ -1692,6 +1693,13 @@ func (a *App) GetSessionEvents(ctx context.Context, sessionId uuid.UUID) (*Sessi &gestureScroll.EndY, &gestureScroll.Direction, + // excpetion + &exception.Handled, + &exception.Fingerprint, + &exception.Foreground, + &exceptionExceptions, + &exceptionThreads, + // lifecycle app &lifecycleApp.Type, @@ -1774,13 +1782,6 @@ func (a *App) GetSessionEvents(ctx context.Context, sessionId uuid.UUID) (*Sessi &anrExceptions, &anrThreads, - // excpetion - &exception.Handled, - &exception.Fingerprint, - &exception.Foreground, - &exceptionExceptions, - &exceptionThreads, - // app exit &appExit.Reason, &appExit.Importance, diff --git a/backend/api/measure/event.go b/backend/api/measure/event.go index 65741f09e..572e6096b 100644 --- a/backend/api/measure/event.go +++ b/backend/api/measure/event.go @@ -62,6 +62,7 @@ type eventreq struct { id uuid.UUID appId uuid.UUID status status + platform string symbolicate map[uuid.UUID]int exceptionIds []int anrIds []int @@ -161,7 +162,7 @@ func (e *eventreq) read(c *gin.Context, appId uuid.UUID) error { return fmt.Errorf(`payload must contain at least 1 event or 1 span`) } - dupeEventMap := make(map[uuid.UUID]struct{}) + dupEvent := make(map[uuid.UUID]struct{}) for i := range events { if events[i] == "" { @@ -175,11 +176,11 @@ func (e *eventreq) read(c *gin.Context, appId uuid.UUID) error { // discard batch if duplicate // event ids found - _, ok := dupeEventMap[ev.ID] + _, ok := dupEvent[ev.ID] if ok { return fmt.Errorf("duplicate event id %q found, discarding batch", ev.ID) } else { - dupeEventMap[ev.ID] = struct{}{} + dupEvent[ev.ID] = struct{}{} } e.bumpSize(int64(len(bytes))) @@ -221,7 +222,7 @@ func (e *eventreq) read(c *gin.Context, appId uuid.UUID) error { e.events = append(e.events, ev) } - dupeSpanMap := make(map[string]struct{}) + dupSpan := make(map[string]struct{}) for i := range spans { if spans[i] == "" { @@ -235,11 +236,11 @@ func (e *eventreq) read(c *gin.Context, appId uuid.UUID) error { // discard batch if duplicate // span ids found - _, ok := dupeSpanMap[sp.SpanID] + _, ok := dupSpan[sp.SpanID] if ok { return fmt.Errorf("duplicate span id %q found, discarding batch", sp.SpanID) } else { - dupeSpanMap[sp.SpanID] = struct{}{} + dupSpan[sp.SpanID] = struct{}{} } e.bumpSize(int64(len(bytes))) @@ -570,8 +571,8 @@ func (e eventreq) validate() error { // if the payload contains any. // // this check is super important to have - // because older SDKs won't ever send these - // attributes. + // because SDKs without support for user + // defined attributes won't ever send these. if !e.events[i].UserDefinedAttribute.Empty() { if err := e.events[i].UserDefinedAttribute.Validate(); err != nil { return err @@ -627,7 +628,7 @@ func (e eventreq) ingestEvents(ctx context.Context) error { return err } anrThreads = string(marshalledThreads) - if err := e.events[i].ANR.ComputeANRFingerprint(); err != nil { + if err := e.events[i].ANR.ComputeFingerprint(); err != nil { return err } } @@ -643,7 +644,7 @@ func (e eventreq) ingestEvents(ctx context.Context) error { return err } exceptionThreads = string(marshalledThreads) - if err := e.events[i].Exception.ComputeExceptionFingerprint(); err != nil { + if err := e.events[i].Exception.ComputeFingerprint(); err != nil { return err } } @@ -2013,6 +2014,7 @@ func PutEvents(c *gin.Context) { msg := `failed to parse event request payload` eventReq := eventreq{ appId: appId, + platform: app.Platform, symbolicate: make(map[uuid.UUID]int), attachments: make(map[uuid.UUID]*attachment), createdAt: time.Now(), diff --git a/backend/api/numeric/numeric.go b/backend/api/numeric/numeric.go index a40c649e1..7cf5f29f7 100644 --- a/backend/api/numeric/numeric.go +++ b/backend/api/numeric/numeric.go @@ -1,5 +1,7 @@ package numeric +import "math" + // AbsInt returns the absolute // value of int n. // @@ -14,3 +16,9 @@ func AbsInt(n int) int { } return n } + +// RoundTwoDecimalsFloat64 rounds the precision +// part of a float64 value to 2 decimals. +func RoundTwoDecimalsFloat64(x float64) float64 { + return math.Ceil(x*100) / 100 +} diff --git a/backend/api/platform/platform.go b/backend/api/platform/platform.go index d4351ec47..6e01230ef 100644 --- a/backend/api/platform/platform.go +++ b/backend/api/platform/platform.go @@ -3,4 +3,5 @@ package platform const ( IOS = "ios" Android = "android" + Unknown = "unknown" ) diff --git a/self-host/sessionator/README.md b/self-host/sessionator/README.md index 04fa15cbe..25d2ddadc 100644 --- a/self-host/sessionator/README.md +++ b/self-host/sessionator/README.md @@ -156,9 +156,11 @@ For this to work, make sure `self-host/.env` has the `AWS_ENDPOINT_URL` environm ### Remove apps completely -With `ingest --clean/--clean-all` commands, only the app's resources are removed, but the apps are not. If you wish to remove an app completely with all its resources also removed. Use the `remove apps` command. +The `ingest --clean/--clean-all` commands only removes the app's resources, but the apps themselves are not removed. If you wish to remove an app completely, use the `remove apps` command. Like below. ```sh # syntax, where xxxx is the id of the app go run . remove apps --id xxxx -``` \ No newline at end of file +``` + +You would need to get the id of the app from the `apps` table in Measure's Postgres database.