diff --git a/backend/api/journey/android.go b/backend/api/journey/android.go index 2f8b1ed0e..40d698d7c 100644 --- a/backend/api/journey/android.go +++ b/backend/api/journey/android.go @@ -217,6 +217,26 @@ func (j *JourneyAndroid) buildGraph() { } } + // ScreenView nodes should also update lastParent to enable + // sequential chaining (ScreenView1 -> ScreenView2 -> ScreenView3) + if currNode.IsScreenView { + lastParent = i + + // if going from screen view to activity, we find the + // last activity and set that as the next activity's + // parent node. + if nextNode.IsActivity { + parentNode := j.GetLastActivity(&currNode) + if parentNode != nil { + lastParent = parentNode.ID + } else { + // did not find a parent activity + // will not create an edge + continue + } + } + } + vkey := j.Nodes[lastParent].Name wkey := nextNode.Name v := j.nodelut[vkey] @@ -553,6 +573,7 @@ func NewJourneyAndroid(events []event.EventField, opts *Options) (journey *Journ c := i for { c-- + // reached the end, we're done if c < 0 { diff --git a/backend/api/journey/android_events_four.json b/backend/api/journey/android_events_four.json index 5fa9f67ee..c65497c50 100644 --- a/backend/api/journey/android_events_four.json +++ b/backend/api/journey/android_events_four.json @@ -1,53 +1,161 @@ [ { - "id": "da1fd321-fdd7-4dc5-983d-fb148d10b545", - "session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e", - "timestamp": "2024-09-18T16:15:25.422Z", + "id": "00000000-0000-0000-0000-000000000001", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.400Z", "type": "lifecycle_activity", "lifecycle_activity": { - "type": "created", - "class_name": "sh.measure.sample.ComposeNavigationActivity", + "type": "resumed", + "class_name": "com.example.ActivityA1", "intent": "", "saved_instance_state": false } }, { - "id": "c23260a4-e767-4153-b242-020da4f3bbf4", - "session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e", - "timestamp": "2024-09-18T16:15:25.437Z", - "type": "lifecycle_activity", - "lifecycle_activity": { + "id": "00000000-0000-0000-0000-000000000002", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.410Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { "type": "resumed", - "class_name": "sh.measure.sample.ComposeNavigationActivity", - "intent": "", - "saved_instance_state": false + "class_name": "com.example.FragmentF1", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000003", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.420Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { + "type": "resumed", + "class_name": "com.example.FragmentF2", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "com.example.FragmentF1", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000004", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.430Z", + "type": "screen_view", + "screen_view": { + "name": "screen_s1" } }, { - "id": "084d900a-41fb-43ac-b74a-8053853cae6b", - "session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e", - "timestamp": "2024-09-18T16:15:25.452Z", + "id": "00000000-0000-0000-0000-000000000005", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.440Z", "type": "screen_view", "screen_view": { - "name": "home" + "name": "screen_s2" } }, { - "id": "084d900a-41fb-43ac-b74a-8053853cae6c", - "session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e", - "timestamp": "2024-09-18T16:15:25.457Z", + "id": "00000000-0000-0000-0000-000000000006", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.450Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { + "type": "resumed", + "class_name": "com.example.FragmentF1", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000007", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.460Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { + "type": "resumed", + "class_name": "com.example.FragmentF2", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "com.example.FragmentF1", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000008", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.470Z", "type": "screen_view", "screen_view": { - "name": "order" + "name": "screen_s3" + } + }, + { + "id": "00000000-0000-0000-0000-000000000009", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.480Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { + "type": "resumed", + "class_name": "com.example.FragmentF2", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "com.example.FragmentF1", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000010", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.490Z", + "type": "lifecycle_fragment", + "lifecycle_fragment": { + "type": "resumed", + "class_name": "com.example.FragmentF1", + "parent_activity": "com.example.ActivityA1", + "parent_fragment": "", + "tag": "" + } + }, + { + "id": "00000000-0000-0000-0000-000000000011", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.500Z", + "type": "screen_view", + "screen_view": { + "name": "screen_s2" + } + }, + { + "id": "00000000-0000-0000-0000-000000000012", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.510Z", + "type": "lifecycle_activity", + "lifecycle_activity": { + "type": "resumed", + "class_name": "com.example.ActivityA2", + "intent": "", + "saved_instance_state": false + } + }, + { + "id": "00000000-0000-0000-0000-000000000013", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.520Z", + "type": "lifecycle_activity", + "lifecycle_activity": { + "type": "resumed", + "class_name": "com.example.ActivityA3", + "intent": "", + "saved_instance_state": false } }, { - "id": "084d900a-41fb-43ac-b74a-8053853cae6d", - "session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e", - "timestamp": "2024-09-18T16:15:25.462Z", + "id": "00000000-0000-0000-0000-000000000014", + "session_id": "10000000-0000-0000-0000-000000000001", + "timestamp": "2024-09-18T16:15:25.530Z", "type": "screen_view", "screen_view": { - "name": "checkout" + "name": "screen_s2" } } -] \ No newline at end of file +] diff --git a/backend/api/journey/android_test.go b/backend/api/journey/android_test.go index ff0fb11e4..9ca05a46c 100644 --- a/backend/api/journey/android_test.go +++ b/backend/api/journey/android_test.go @@ -2009,17 +2009,17 @@ func TestNewJourneyAndroidANRsOne(t *testing.T) { } func TestNewJourneyAndroidScreenViewsFour(t *testing.T) { + // Sequence: A1 -> F1 -> F2 -> S1 -> S2 -> F1 -> F2 -> S3 -> F2 -> F1 -> S2 -> A2 -> A3 -> S2 events, err := readEvents("android_events_four.json") if err != nil { - panic(err) + t.Fatalf("Error reading events: %v", err) } journey := NewJourneyAndroid(events, &Options{ BiGraph: true, }) - // Verify correct number of nodes (1 activity + 3 screen views = 4 nodes) - expectedOrder := 4 + expectedOrder := 8 gotOrder := journey.Graph.Order() if expectedOrder != gotOrder { @@ -2027,31 +2027,63 @@ func TestNewJourneyAndroidScreenViewsFour(t *testing.T) { } vertices := journey.GetNodeVertices() - screenViewNodes := make(map[string]int) - var activityVertex int + nodeMap := make(map[string]int) for _, vertex := range vertices { nodeName := journey.GetNodeName(vertex) - switch nodeName { - case "home", "order", "checkout": - screenViewNodes[nodeName] = vertex - case "sh.measure.sample.ComposeNavigationActivity": - activityVertex = vertex - } + nodeMap[nodeName] = vertex } - // Verify graph structure: activity should have edges to all screen view nodes - expectedGraphString := "4 [(0 1) (0 2) (0 3)]" - gotGraphString := journey.Graph.String() + a1 := nodeMap["com.example.ActivityA1"] + f1 := nodeMap["com.example.FragmentF1"] + f2 := nodeMap["com.example.FragmentF2"] + s1 := nodeMap["screen_s1"] + s2 := nodeMap["screen_s2"] + s3 := nodeMap["screen_s3"] + a2 := nodeMap["com.example.ActivityA2"] + a3 := nodeMap["com.example.ActivityA3"] - if expectedGraphString != gotGraphString { - t.Errorf("Expected graph %q, got %q", expectedGraphString, gotGraphString) + if !journey.Graph.Edge(a1, f1) { + t.Errorf("Expected edge A1->F1") } - // Verify edges from activity to each screen view - for screenName, screenVertex := range screenViewNodes { - if !journey.Graph.Edge(activityVertex, screenVertex) { - t.Errorf("Expected edge from ComposeNavigationActivity to %s screen view", screenName) - } + if !journey.Graph.Edge(f1, f2) { + t.Errorf("Expected edge F1->F2") + } + + if !journey.Graph.Edge(f1, s1) { + t.Errorf("Expected edge F1->S1") + } + + if !journey.Graph.Edge(s1, s2) { + t.Errorf("Expected edge S1->S2") + } + + if !journey.Graph.Edge(s2, f1) { + t.Errorf("Expected edge S2->F1") + } + + if !journey.Graph.Edge(f1, s2) { + t.Errorf("Expected edge F1->S2") + } + + if !journey.Graph.Edge(f1, s3) { + t.Errorf("Expected edge F1->S3") + } + + if !journey.Graph.Edge(s3, f2) { + t.Errorf("Expected edge S3->F2") + } + + if !journey.Graph.Edge(a1, a2) { + t.Errorf("Expected edge A1->A2") + } + + if !journey.Graph.Edge(a2, a3) { + t.Errorf("Expected edge A2->A3") + } + + if !journey.Graph.Edge(a3, s2) { + t.Errorf("Expected edge A3->S2") } } diff --git a/backend/api/journey/ios.go b/backend/api/journey/ios.go index 8c95dff39..57651a09b 100644 --- a/backend/api/journey/ios.go +++ b/backend/api/journey/ios.go @@ -164,6 +164,25 @@ func (j *JourneyiOS) buildGraph() { lastParent = i } + // ScreenView nodes should also update lastParent to enable + // sequential chaining (ScreenView1 -> ScreenView2 -> ScreenView3) + if currNode.IsScreenView { + lastParent = i + + // if going from screen view to view controller/swiftui, we find the + // last view controller and set that as the next node's parent + if nextNode.IsViewController || nextNode.IsSwiftUI { + parentNode := j.GetLastViewController(&currNode) + if parentNode != nil { + lastParent = parentNode.ID + } else { + // did not find a parent view controller + // will not create an edge + continue + } + } + } + vkey := j.Nodes[lastParent].Name wkey := nextNode.Name v := j.nodelut[vkey] @@ -354,6 +373,26 @@ func (j JourneyiOS) GetLastView(node *NodeiOS) (parent *NodeiOS) { return } +// GetLastViewController finds the last ViewController or SwiftUI +// node by traversing towards start direction, excluding ScreenViews. +func (j JourneyiOS) GetLastViewController(node *NodeiOS) (parent *NodeiOS) { + c := node.ID + + for { + c-- + + if c < 0 { + break + } + + if j.Nodes[c].IsViewController || j.Nodes[c].IsSwiftUI { + return &j.Nodes[c] + } + } + + return +} + // NewJourneyiOS creates a journey graph object // from a list of iOS specific events. func NewJourneyiOS(events []event.EventField, opts *Options) (journey *JourneyiOS) { diff --git a/backend/api/journey/ios_test.go b/backend/api/journey/ios_test.go index 355c296cc..e2fef7a61 100644 --- a/backend/api/journey/ios_test.go +++ b/backend/api/journey/ios_test.go @@ -88,16 +88,27 @@ func TestNewJourneyiOSWithScreenViews(t *testing.T) { } } - expectedGraphString := "4 [(0 1) (0 2) (0 3)]" + expectedGraphString := "4 [(0 1) (1 2) (2 3)]" gotGraphString := journey.Graph.String() if expectedGraphString != gotGraphString { t.Errorf("Expected graph %q, got %q", expectedGraphString, gotGraphString) } - for screenName, screenVertex := range screenViewNodes { - if !journey.Graph.Edge(viewControllerVertex, screenVertex) { - t.Errorf("Expected edge from ControlsViewController to %s screen view", screenName) - } + // Verify sequential edges + homeVertex := screenViewNodes["home"] + orderVertex := screenViewNodes["order"] + checkoutVertex := screenViewNodes["checkout"] + + if !journey.Graph.Edge(viewControllerVertex, homeVertex) { + t.Errorf("Expected edge from ControlsViewController to home screen view") + } + + if !journey.Graph.Edge(homeVertex, orderVertex) { + t.Errorf("Expected edge from home to order screen view (sequential)") + } + + if !journey.Graph.Edge(orderVertex, checkoutVertex) { + t.Errorf("Expected edge from order to checkout screen view (sequential)") } }