From f7661ef8ff22c257d2bd1a354ab129b0933c7dc7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 19 Mar 2026 17:12:03 +0000 Subject: [PATCH 1/3] Implement fallback frame matching by function name and module Fixes VROOM-5B This commit adds fallback frame matching functionality to handle cases where the fingerprint computed by the client SDK doesn't match any frame in the profile data due to differences in fingerprint computation. Key changes: - Added FindFrameByFingerprintWithFallback() helper function in frame package that tries alternative fingerprint calculations (raw package, file, module variations) when exact fingerprint match fails - Updated GetFrameWithFingerprint() in SampleChunk, AndroidChunk, sample.Profile, and profile.Android to use fallback matching - Added structured logging to track when fallback matching is used, including target fingerprint, matched frame details, and profile/chunk IDs for debugging - Added comprehensive tests for the fallback matching functionality - Graceful degradation: if both exact and fallback matching fail, returns the existing ErrFrameNotFound error The fallback approach computes fingerprint variations for each frame by trying different normalizations of module/package names (raw package path, file path, empty module, etc.) to account for differences in how client SDKs and vroom compute fingerprints. --- internal/chunk/android.go | 24 +++++ internal/chunk/sample.go | 18 ++++ internal/frame/frame.go | 81 ++++++++++++++++- internal/frame/frame_fallback_test.go | 126 ++++++++++++++++++++++++++ internal/profile/android.go | 22 +++++ internal/sample/sample.go | 17 ++++ 6 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 internal/frame/frame_fallback_test.go diff --git a/internal/chunk/android.go b/internal/chunk/android.go index ae918538..b60bca79 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..f47e3a31 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..78ef13cf 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..4caa52ba --- /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..63a40e03 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..30d47856 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 } From 2715e7ef32ba457abe6c97e023fb4955ae93fdd9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 19 Mar 2026 17:14:30 +0000 Subject: [PATCH 2/3] Fix formatting and linting issues - Run gofmt on all modified files - Add period to comment in frame_fallback_test.go --- internal/chunk/android.go | 6 +++--- internal/chunk/sample.go | 4 ++-- internal/frame/frame.go | 22 +++++++++++----------- internal/frame/frame_fallback_test.go | 24 ++++++++++++------------ 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/chunk/android.go b/internal/chunk/android.go index b60bca79..c1b66ce7 100644 --- a/internal/chunk/android.go +++ b/internal/chunk/android.go @@ -130,13 +130,13 @@ func (c AndroidChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error 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 { @@ -151,7 +151,7 @@ func (c AndroidChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error ) return matchedFrame, nil } - + return frame.Frame{}, frame.ErrFrameNotFound } diff --git a/internal/chunk/sample.go b/internal/chunk/sample.go index f47e3a31..627b483f 100644 --- a/internal/chunk/sample.go +++ b/internal/chunk/sample.go @@ -261,7 +261,7 @@ func (c SampleChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) return f, nil } } - + // Try fallback with fingerprint variations matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(c.Profile.Frames, target) if err == nil && usedFallback { @@ -276,6 +276,6 @@ func (c SampleChunk) GetFrameWithFingerprint(target uint32) (frame.Frame, error) ) return matchedFrame, nil } - + return frame.Frame{}, frame.ErrFrameNotFound } diff --git a/internal/frame/frame.go b/internal/frame/frame.go index 78ef13cf..eb5524bc 100644 --- a/internal/frame/frame.go +++ b/internal/frame/frame.go @@ -23,8 +23,8 @@ var ( "hermes": {}, } - ErrFrameNotFound = errors.New("Unable to find matching frame") - ErrFrameNotFoundWithFallback = errors.New("Unable to find matching frame even with fallback") + ErrFrameNotFound = errors.New("Unable to find matching frame") + ErrFrameNotFoundWithFallback = errors.New("Unable to find matching frame even with fallback") ) type ( @@ -248,10 +248,10 @@ func (f Frame) Fingerprint() uint32 { 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() @@ -260,7 +260,7 @@ func computeFingerprintVariations(f Frame) []uint32 { 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() @@ -269,7 +269,7 @@ func computeFingerprintVariations(f Frame) []uint32 { 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() @@ -278,19 +278,19 @@ func computeFingerprintVariations(f Frame) []uint32 { 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 } @@ -305,7 +305,7 @@ func FindFrameByFingerprintWithFallback(frames []Frame, targetFingerprint uint32 return f, false, nil } } - + // Second pass: try fingerprint variations (fallback) for _, f := range frames { variations := computeFingerprintVariations(f) @@ -315,7 +315,7 @@ func FindFrameByFingerprintWithFallback(frames []Frame, targetFingerprint uint32 } } } - + return Frame{}, false, ErrFrameNotFoundWithFallback } diff --git a/internal/frame/frame_fallback_test.go b/internal/frame/frame_fallback_test.go index 4caa52ba..173752ee 100644 --- a/internal/frame/frame_fallback_test.go +++ b/internal/frame/frame_fallback_test.go @@ -7,12 +7,12 @@ import ( func TestFindFrameByFingerprintWithFallback(t *testing.T) { tests := []struct { - name string - frames []Frame - targetFingerprint uint32 - expectMatch bool - expectFallback bool - expectedFunction string + name string + frames []Frame + targetFingerprint uint32 + expectMatch bool + expectFallback bool + expectedFunction string }{ { name: "exact match", @@ -58,7 +58,7 @@ func TestFindFrameByFingerprintWithFallback(t *testing.T) { 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) @@ -80,9 +80,9 @@ func TestFindFrameByFingerprintWithFallback(t *testing.T) { func TestComputeFingerprintVariations(t *testing.T) { tests := []struct { - name string - frame Frame - minVariations int + name string + frame Frame + minVariations int }{ { name: "frame with module and function", @@ -107,7 +107,7 @@ func TestComputeFingerprintVariations(t *testing.T) { 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") @@ -116,7 +116,7 @@ func TestComputeFingerprintVariations(t *testing.T) { } } -// Helper function to compute fingerprint from file and function +// Helper function to compute fingerprint from file and function. func computeFingerprintFromFileAndFunction(file, function string) uint32 { h := fnv.New64() h.Write([]byte(file)) From 248f1aee36e2aa47b05d83bc15e5d716ebab724d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 19 Mar 2026 17:16:01 +0000 Subject: [PATCH 3/3] Fix remaining formatting issues in sample.go and android.go --- internal/profile/android.go | 6 +++--- internal/sample/sample.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/profile/android.go b/internal/profile/android.go index 63a40e03..5d546ffc 100644 --- a/internal/profile/android.go +++ b/internal/profile/android.go @@ -678,13 +678,13 @@ func (p Android) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { 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 { @@ -697,7 +697,7 @@ func (p Android) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { ) 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 30d47856..420d6e44 100644 --- a/internal/sample/sample.go +++ b/internal/sample/sample.go @@ -454,7 +454,7 @@ func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { return f, nil } } - + // Try fallback with fingerprint variations matchedFrame, usedFallback, err := frame.FindFrameByFingerprintWithFallback(p.Trace.Frames, target) if err == nil && usedFallback { @@ -468,7 +468,7 @@ func (p *Profile) GetFrameWithFingerprint(target uint32) (frame.Frame, error) { ) return matchedFrame, nil } - + return frame.Frame{}, frame.ErrFrameNotFound }