Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions backend/api/journey/android.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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 {
Expand Down
160 changes: 134 additions & 26 deletions backend/api/journey/android_events_four.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
]
74 changes: 53 additions & 21 deletions backend/api/journey/android_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2009,49 +2009,81 @@ 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 {
t.Errorf("Expected %d vertices, but got %d", expectedOrder, gotOrder)
}

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")
}
}
39 changes: 39 additions & 0 deletions backend/api/journey/ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading