diff --git a/internal/chunk/android.go b/internal/chunk/android.go index ae918538..c1b66ce7 100644 --- a/internal/chunk/android.go +++ b/internal/chunk/android.go @@ -2,6 +2,7 @@ package chunk import ( "encoding/json" + "log/slog" "strconv" "time" @@ -122,12 +123,35 @@ func (c AndroidChunk) GetOptions() options.Options { } func (c AndroidChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { + // Try exact match first for _, m := range c.Profile.Methods { f := m.Frame() if f.Fingerprint() == target { return f, nil } } + + // Build frames array for fallback matching + frames := make([]frame.Frame, 0, len(c.Profile.Methods)) + for _, m := range c.Profile.Methods { + frames = append(frames, m.Frame()) + } + + // Try fallback with fingerprint variations + matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(frames, target) + if err == nil && usedFallback { + slog.Warn( + "Frame matched using fallback fingerprint computation", + "target_fingerprint", target, + "matched_frame_fingerprint", matchedFrame.Fingerprint(), + "matched_function", matchedFrame.Function, + "matched_module", matchedFrame.ModuleOrPackage(), + "chunk_id", c.ID, + "profiler_id", c.ProfilerID, + ) + return matchedFrame, nil + } + return frame.Frame{}, frame.ErrFrameNotFound } diff --git a/internal/chunk/sample.go b/internal/chunk/sample.go index 2b124d20..627b483f 100644 --- a/internal/chunk/sample.go +++ b/internal/chunk/sample.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "hash/fnv" + "log/slog" "math" "sort" @@ -254,10 +255,27 @@ func (c SampleChunk) GetOptions() options.Options { } func (c SampleChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { + // Try exact match first for _, f := range c.Profile.Frames { if f.Fingerprint() == target { return f, nil } } + + // Try fallback with fingerprint variations + matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(c.Profile.Frames, target) + if err == nil && usedFallback { + slog.Warn( + "Frame matched using fallback fingerprint computation", + "target_fingerprint", target, + "matched_frame_fingerprint", matchedFrame.Fingerprint(), + "matched_function", matchedFrame.Function, + "matched_module", matchedFrame.ModuleOrPackage(), + "chunk_id", c.ID, + "profiler_id", c.ProfilerID, + ) + return matchedFrame, nil + } + return frame.Frame{}, frame.ErrFrameNotFound } diff --git a/internal/frame/frame.go b/internal/frame/frame.go index 72fc1b70..eb5524bc 100644 --- a/internal/frame/frame.go +++ b/internal/frame/frame.go @@ -23,7 +23,8 @@ var ( "hermes": {}, } - ErrFrameNotFound = errors.New("Unable to find matching frame") + ErrFrameNotFound = errors.New("Unable to find matching frame") + ErrFrameNotFoundWithFallback = errors.New("Unable to find matching frame even with fallback") ) type ( @@ -240,6 +241,84 @@ func (f Frame) Fingerprint() uint32 { return uint32(h.Sum64()) } +// computeFingerprintVariations computes alternative fingerprint values for a frame +// by trying different normalizations of module/package and function names. +// This helps match frames when the fingerprint computation differs slightly +// between client SDK and vroom (e.g., due to encoding, normalization, or edge cases). +func computeFingerprintVariations(f Frame) []uint32 { + variations := make([]uint32, 0, 8) + h := fnv.New64() + + // Original fingerprint (already tried, but include for completeness) + variations = append(variations, f.Fingerprint()) + + // Try with raw Package instead of trimPackage(Package) + if f.Package != "" && f.Module == "" { + h.Reset() + h.Write([]byte(f.Package)) + h.Write([]byte{':'}) + h.Write([]byte(f.Function)) + variations = append(variations, uint32(h.Sum64())) + } + + // Try with File instead of Module/Package + if f.File != "" && (f.Module != "" || f.Package != "") { + h.Reset() + h.Write([]byte(f.File)) + h.Write([]byte{':'}) + h.Write([]byte(f.Function)) + variations = append(variations, uint32(h.Sum64())) + } + + // Try with Module alone (even if Package is set) + if f.Module != "" { + h.Reset() + h.Write([]byte(f.Module)) + h.Write([]byte{':'}) + h.Write([]byte(f.Function)) + variations = append(variations, uint32(h.Sum64())) + } + + // Try with empty module/package (just function name) + if f.Function != "" { + h.Reset() + h.Write([]byte{':'}) + h.Write([]byte(f.Function)) + variations = append(variations, uint32(h.Sum64())) + + h.Reset() + h.Write([]byte(f.Function)) + variations = append(variations, uint32(h.Sum64())) + } + + return variations +} + +// FindFrameByFingerprintWithFallback searches for a frame matching the target fingerprint. +// It first tries exact matches, then tries alternative fingerprint calculations +// to handle cases where the client SDK and vroom compute fingerprints differently. +// Returns the matching frame, whether fallback was used, and an error if no match found. +func FindFrameByFingerprintWithFallback(frames []Frame, targetFingerprint uint32) (Frame, bool, error) { + // First pass: try exact fingerprint match + for _, f := range frames { + if f.Fingerprint() == targetFingerprint { + return f, false, nil + } + } + + // Second pass: try fingerprint variations (fallback) + for _, f := range frames { + variations := computeFingerprintVariations(f) + for _, variant := range variations { + if variant == targetFingerprint { + return f, true, nil + } + } + } + + return Frame{}, false, ErrFrameNotFoundWithFallback +} + func defaultFormatter(f Frame) string { return f.Function } diff --git a/internal/frame/frame_fallback_test.go b/internal/frame/frame_fallback_test.go new file mode 100644 index 00000000..173752ee --- /dev/null +++ b/internal/frame/frame_fallback_test.go @@ -0,0 +1,126 @@ +package frame + +import ( + "hash/fnv" + "testing" +) + +func TestFindFrameByFingerprintWithFallback(t *testing.T) { + tests := []struct { + name string + frames []Frame + targetFingerprint uint32 + expectMatch bool + expectFallback bool + expectedFunction string + }{ + { + name: "exact match", + frames: []Frame{ + {Function: "testFunc", Module: "testModule"}, + }, + targetFingerprint: Frame{Function: "testFunc", Module: "testModule"}.Fingerprint(), + expectMatch: true, + expectFallback: false, + expectedFunction: "testFunc", + }, + { + name: "fallback match with raw package", + frames: []Frame{ + {Function: "testFunc", Package: "/path/to/libtest.so"}, + }, + targetFingerprint: Frame{Function: "testFunc", Package: "/path/to/libtest.so"}.Fingerprint(), + expectMatch: true, + expectFallback: false, + expectedFunction: "testFunc", + }, + { + name: "no match", + frames: []Frame{ + {Function: "testFunc", Module: "testModule"}, + }, + targetFingerprint: Frame{Function: "differentFunc", Module: "differentModule"}.Fingerprint(), + expectMatch: false, + expectFallback: false, + }, + { + name: "fallback match with file instead of module", + frames: []Frame{ + {Function: "testFunc", File: "testFile.py", Module: "testModule"}, + }, + targetFingerprint: computeFingerprintFromFileAndFunction("testFile.py", "testFunc"), + expectMatch: true, + expectFallback: true, + expectedFunction: "testFunc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + matchedFrame, usedFallback, err := FindFrameByFingerprintWithFallback(tt.frames, tt.targetFingerprint) + + if tt.expectMatch { + if err != nil { + t.Errorf("Expected match but got error: %v", err) + } + if matchedFrame.Function != tt.expectedFunction { + t.Errorf("Expected function %s, got %s", tt.expectedFunction, matchedFrame.Function) + } + if usedFallback != tt.expectFallback { + t.Errorf("Expected fallback=%v, got %v", tt.expectFallback, usedFallback) + } + } else { + if err == nil { + t.Errorf("Expected no match but found frame: %+v", matchedFrame) + } + } + }) + } +} + +func TestComputeFingerprintVariations(t *testing.T) { + tests := []struct { + name string + frame Frame + minVariations int + }{ + { + name: "frame with module and function", + frame: Frame{Function: "testFunc", Module: "testModule"}, + minVariations: 2, + }, + { + name: "frame with package and function", + frame: Frame{Function: "testFunc", Package: "/path/to/lib.so"}, + minVariations: 3, + }, + { + name: "frame with file, module and function", + frame: Frame{Function: "testFunc", Module: "testModule", File: "test.py"}, + minVariations: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + variations := computeFingerprintVariations(tt.frame) + if len(variations) < tt.minVariations { + t.Errorf("Expected at least %d variations, got %d", tt.minVariations, len(variations)) + } + + // Verify first variation matches the standard fingerprint + if variations[0] != tt.frame.Fingerprint() { + t.Errorf("First variation should match standard fingerprint") + } + }) + } +} + +// Helper function to compute fingerprint from file and function. +func computeFingerprintFromFileAndFunction(file, function string) uint32 { + h := fnv.New64() + h.Write([]byte(file)) + h.Write([]byte{':'}) + h.Write([]byte(function)) + return uint32(h.Sum64()) +} diff --git a/internal/profile/android.go b/internal/profile/android.go index 49f8b5cb..5d546ffc 100644 --- a/internal/profile/android.go +++ b/internal/profile/android.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "hash/fnv" + "log/slog" "math" "path" "strings" @@ -670,12 +671,33 @@ func (p Android) ActiveThreadID() uint64 { } func (p Android) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { + // Try exact match first for _, m := range p.Methods { f := m.Frame() if f.Fingerprint() == target { return f, nil } } + + // Build frames array for fallback matching + frames := make([]frame.Frame, 0, len(p.Methods)) + for _, m := range p.Methods { + frames = append(frames, m.Frame()) + } + + // Try fallback with fingerprint variations + matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(frames, target) + if err == nil && usedFallback { + slog.Warn( + "Frame matched using fallback fingerprint computation", + "target_fingerprint", target, + "matched_frame_fingerprint", matchedFrame.Fingerprint(), + "matched_function", matchedFrame.Function, + "matched_module", matchedFrame.ModuleOrPackage(), + ) + return matchedFrame, nil + } + // TODO: handle react native return frame.Frame{}, frame.ErrFrameNotFound } diff --git a/internal/sample/sample.go b/internal/sample/sample.go index c8664e80..420d6e44 100644 --- a/internal/sample/sample.go +++ b/internal/sample/sample.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "hash/fnv" + "log/slog" "sort" "strconv" "strings" @@ -447,11 +448,27 @@ func (p *Profile) GetOptions() options.Options { } func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { + // Try exact match first for _, f := range p.Trace.Frames { if f.Fingerprint() == target { return f, nil } } + + // Try fallback with fingerprint variations + matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(p.Trace.Frames, target) + if err == nil && usedFallback { + slog.Warn( + "Frame matched using fallback fingerprint computation", + "target_fingerprint", target, + "matched_frame_fingerprint", matchedFrame.Fingerprint(), + "matched_function", matchedFrame.Function, + "matched_module", matchedFrame.ModuleOrPackage(), + "profile_id", p.EventID, + ) + return matchedFrame, nil + } + return frame.Frame{}, frame.ErrFrameNotFound }