Skip to content

Commit 56bc772

Browse files
committed
fix(backend): enable ScreenView sequential chaining in journey
1 parent 6ee4dec commit 56bc772

File tree

6 files changed

+268
-52
lines changed

6 files changed

+268
-52
lines changed

android/sample/src/main/java/sh/measure/sample/ExceptionDemoActivity.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ class ExceptionDemoActivity : AppCompatActivity(), MsrShakeListener {
183183

184184

185185
span.setStatus(SpanStatus.Ok).end()
186+
187+
Measure.trackScreenView("Screen A")
188+
Measure.trackScreenView("Screen B")
189+
Measure.trackScreenView("Screen C")
190+
Measure.trackScreenView("Screen D")
186191
}
187192

188193
private fun infiniteLoop() {

backend/api/journey/android.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ func (j *JourneyAndroid) buildGraph() {
217217
}
218218
}
219219

220+
// ScreenView nodes should also update lastParent to enable
221+
// sequential chaining (ScreenView1 -> ScreenView2 -> ScreenView3)
222+
if currNode.IsScreenView {
223+
lastParent = i
224+
225+
// if going from screen view to activity, we find the
226+
// last activity and set that as the next activity's
227+
// parent node.
228+
if nextNode.IsActivity {
229+
parentNode := j.GetLastActivity(&currNode)
230+
if parentNode != nil {
231+
lastParent = parentNode.ID
232+
} else {
233+
// did not find a parent activity
234+
// will not create an edge
235+
continue
236+
}
237+
}
238+
}
239+
220240
vkey := j.Nodes[lastParent].Name
221241
wkey := nextNode.Name
222242
v := j.nodelut[vkey]
@@ -553,6 +573,7 @@ func NewJourneyAndroid(events []event.EventField, opts *Options) (journey *Journ
553573
c := i
554574
for {
555575
c--
576+
556577

557578
// reached the end, we're done
558579
if c < 0 {
Lines changed: 134 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,161 @@
11
[
22
{
3-
"id": "da1fd321-fdd7-4dc5-983d-fb148d10b545",
4-
"session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e",
5-
"timestamp": "2024-09-18T16:15:25.422Z",
3+
"id": "00000000-0000-0000-0000-000000000001",
4+
"session_id": "10000000-0000-0000-0000-000000000001",
5+
"timestamp": "2024-09-18T16:15:25.400Z",
66
"type": "lifecycle_activity",
77
"lifecycle_activity": {
8-
"type": "created",
9-
"class_name": "sh.measure.sample.ComposeNavigationActivity",
8+
"type": "resumed",
9+
"class_name": "com.example.ActivityA1",
1010
"intent": "",
1111
"saved_instance_state": false
1212
}
1313
},
1414
{
15-
"id": "c23260a4-e767-4153-b242-020da4f3bbf4",
16-
"session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e",
17-
"timestamp": "2024-09-18T16:15:25.437Z",
18-
"type": "lifecycle_activity",
19-
"lifecycle_activity": {
15+
"id": "00000000-0000-0000-0000-000000000002",
16+
"session_id": "10000000-0000-0000-0000-000000000001",
17+
"timestamp": "2024-09-18T16:15:25.410Z",
18+
"type": "lifecycle_fragment",
19+
"lifecycle_fragment": {
2020
"type": "resumed",
21-
"class_name": "sh.measure.sample.ComposeNavigationActivity",
22-
"intent": "",
23-
"saved_instance_state": false
21+
"class_name": "com.example.FragmentF1",
22+
"parent_activity": "com.example.ActivityA1",
23+
"parent_fragment": "",
24+
"tag": ""
25+
}
26+
},
27+
{
28+
"id": "00000000-0000-0000-0000-000000000003",
29+
"session_id": "10000000-0000-0000-0000-000000000001",
30+
"timestamp": "2024-09-18T16:15:25.420Z",
31+
"type": "lifecycle_fragment",
32+
"lifecycle_fragment": {
33+
"type": "resumed",
34+
"class_name": "com.example.FragmentF2",
35+
"parent_activity": "com.example.ActivityA1",
36+
"parent_fragment": "com.example.FragmentF1",
37+
"tag": ""
38+
}
39+
},
40+
{
41+
"id": "00000000-0000-0000-0000-000000000004",
42+
"session_id": "10000000-0000-0000-0000-000000000001",
43+
"timestamp": "2024-09-18T16:15:25.430Z",
44+
"type": "screen_view",
45+
"screen_view": {
46+
"name": "screen_s1"
2447
}
2548
},
2649
{
27-
"id": "084d900a-41fb-43ac-b74a-8053853cae6b",
28-
"session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e",
29-
"timestamp": "2024-09-18T16:15:25.452Z",
50+
"id": "00000000-0000-0000-0000-000000000005",
51+
"session_id": "10000000-0000-0000-0000-000000000001",
52+
"timestamp": "2024-09-18T16:15:25.440Z",
3053
"type": "screen_view",
3154
"screen_view": {
32-
"name": "home"
55+
"name": "screen_s2"
3356
}
3457
},
3558
{
36-
"id": "084d900a-41fb-43ac-b74a-8053853cae6c",
37-
"session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e",
38-
"timestamp": "2024-09-18T16:15:25.457Z",
59+
"id": "00000000-0000-0000-0000-000000000006",
60+
"session_id": "10000000-0000-0000-0000-000000000001",
61+
"timestamp": "2024-09-18T16:15:25.450Z",
62+
"type": "lifecycle_fragment",
63+
"lifecycle_fragment": {
64+
"type": "resumed",
65+
"class_name": "com.example.FragmentF1",
66+
"parent_activity": "com.example.ActivityA1",
67+
"parent_fragment": "",
68+
"tag": ""
69+
}
70+
},
71+
{
72+
"id": "00000000-0000-0000-0000-000000000007",
73+
"session_id": "10000000-0000-0000-0000-000000000001",
74+
"timestamp": "2024-09-18T16:15:25.460Z",
75+
"type": "lifecycle_fragment",
76+
"lifecycle_fragment": {
77+
"type": "resumed",
78+
"class_name": "com.example.FragmentF2",
79+
"parent_activity": "com.example.ActivityA1",
80+
"parent_fragment": "com.example.FragmentF1",
81+
"tag": ""
82+
}
83+
},
84+
{
85+
"id": "00000000-0000-0000-0000-000000000008",
86+
"session_id": "10000000-0000-0000-0000-000000000001",
87+
"timestamp": "2024-09-18T16:15:25.470Z",
3988
"type": "screen_view",
4089
"screen_view": {
41-
"name": "order"
90+
"name": "screen_s3"
91+
}
92+
},
93+
{
94+
"id": "00000000-0000-0000-0000-000000000009",
95+
"session_id": "10000000-0000-0000-0000-000000000001",
96+
"timestamp": "2024-09-18T16:15:25.480Z",
97+
"type": "lifecycle_fragment",
98+
"lifecycle_fragment": {
99+
"type": "resumed",
100+
"class_name": "com.example.FragmentF2",
101+
"parent_activity": "com.example.ActivityA1",
102+
"parent_fragment": "com.example.FragmentF1",
103+
"tag": ""
104+
}
105+
},
106+
{
107+
"id": "00000000-0000-0000-0000-000000000010",
108+
"session_id": "10000000-0000-0000-0000-000000000001",
109+
"timestamp": "2024-09-18T16:15:25.490Z",
110+
"type": "lifecycle_fragment",
111+
"lifecycle_fragment": {
112+
"type": "resumed",
113+
"class_name": "com.example.FragmentF1",
114+
"parent_activity": "com.example.ActivityA1",
115+
"parent_fragment": "",
116+
"tag": ""
117+
}
118+
},
119+
{
120+
"id": "00000000-0000-0000-0000-000000000011",
121+
"session_id": "10000000-0000-0000-0000-000000000001",
122+
"timestamp": "2024-09-18T16:15:25.500Z",
123+
"type": "screen_view",
124+
"screen_view": {
125+
"name": "screen_s2"
126+
}
127+
},
128+
{
129+
"id": "00000000-0000-0000-0000-000000000012",
130+
"session_id": "10000000-0000-0000-0000-000000000001",
131+
"timestamp": "2024-09-18T16:15:25.510Z",
132+
"type": "lifecycle_activity",
133+
"lifecycle_activity": {
134+
"type": "resumed",
135+
"class_name": "com.example.ActivityA2",
136+
"intent": "",
137+
"saved_instance_state": false
138+
}
139+
},
140+
{
141+
"id": "00000000-0000-0000-0000-000000000013",
142+
"session_id": "10000000-0000-0000-0000-000000000001",
143+
"timestamp": "2024-09-18T16:15:25.520Z",
144+
"type": "lifecycle_activity",
145+
"lifecycle_activity": {
146+
"type": "resumed",
147+
"class_name": "com.example.ActivityA3",
148+
"intent": "",
149+
"saved_instance_state": false
42150
}
43151
},
44152
{
45-
"id": "084d900a-41fb-43ac-b74a-8053853cae6d",
46-
"session_id": "1d72df05-44ea-4d6f-8306-7b9b5a5b600e",
47-
"timestamp": "2024-09-18T16:15:25.462Z",
153+
"id": "00000000-0000-0000-0000-000000000014",
154+
"session_id": "10000000-0000-0000-0000-000000000001",
155+
"timestamp": "2024-09-18T16:15:25.530Z",
48156
"type": "screen_view",
49157
"screen_view": {
50-
"name": "checkout"
158+
"name": "screen_s2"
51159
}
52160
}
53-
]
161+
]

backend/api/journey/android_test.go

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,49 +2009,81 @@ func TestNewJourneyAndroidANRsOne(t *testing.T) {
20092009
}
20102010

20112011
func TestNewJourneyAndroidScreenViewsFour(t *testing.T) {
2012+
// Sequence: A1 -> F1 -> F2 -> S1 -> S2 -> F1 -> F2 -> S3 -> F2 -> F1 -> S2 -> A2 -> A3 -> S2
20122013
events, err := readEvents("android_events_four.json")
20132014
if err != nil {
2014-
panic(err)
2015+
t.Fatalf("Error reading events: %v", err)
20152016
}
20162017

20172018
journey := NewJourneyAndroid(events, &Options{
20182019
BiGraph: true,
20192020
})
20202021

2021-
// Verify correct number of nodes (1 activity + 3 screen views = 4 nodes)
2022-
expectedOrder := 4
2022+
expectedOrder := 8
20232023
gotOrder := journey.Graph.Order()
20242024

20252025
if expectedOrder != gotOrder {
20262026
t.Errorf("Expected %d vertices, but got %d", expectedOrder, gotOrder)
20272027
}
20282028

20292029
vertices := journey.GetNodeVertices()
2030-
screenViewNodes := make(map[string]int)
2031-
var activityVertex int
2030+
nodeMap := make(map[string]int)
20322031

20332032
for _, vertex := range vertices {
20342033
nodeName := journey.GetNodeName(vertex)
2035-
switch nodeName {
2036-
case "home", "order", "checkout":
2037-
screenViewNodes[nodeName] = vertex
2038-
case "sh.measure.sample.ComposeNavigationActivity":
2039-
activityVertex = vertex
2040-
}
2034+
nodeMap[nodeName] = vertex
20412035
}
20422036

2043-
// Verify graph structure: activity should have edges to all screen view nodes
2044-
expectedGraphString := "4 [(0 1) (0 2) (0 3)]"
2045-
gotGraphString := journey.Graph.String()
2037+
a1 := nodeMap["com.example.ActivityA1"]
2038+
f1 := nodeMap["com.example.FragmentF1"]
2039+
f2 := nodeMap["com.example.FragmentF2"]
2040+
s1 := nodeMap["screen_s1"]
2041+
s2 := nodeMap["screen_s2"]
2042+
s3 := nodeMap["screen_s3"]
2043+
a2 := nodeMap["com.example.ActivityA2"]
2044+
a3 := nodeMap["com.example.ActivityA3"]
20462045

2047-
if expectedGraphString != gotGraphString {
2048-
t.Errorf("Expected graph %q, got %q", expectedGraphString, gotGraphString)
2046+
if !journey.Graph.Edge(a1, f1) {
2047+
t.Errorf("Expected edge A1->F1")
20492048
}
20502049

2051-
// Verify edges from activity to each screen view
2052-
for screenName, screenVertex := range screenViewNodes {
2053-
if !journey.Graph.Edge(activityVertex, screenVertex) {
2054-
t.Errorf("Expected edge from ComposeNavigationActivity to %s screen view", screenName)
2055-
}
2050+
if !journey.Graph.Edge(f1, f2) {
2051+
t.Errorf("Expected edge F1->F2")
2052+
}
2053+
2054+
if !journey.Graph.Edge(f1, s1) {
2055+
t.Errorf("Expected edge F1->S1")
2056+
}
2057+
2058+
if !journey.Graph.Edge(s1, s2) {
2059+
t.Errorf("Expected edge S1->S2")
2060+
}
2061+
2062+
if !journey.Graph.Edge(s2, f1) {
2063+
t.Errorf("Expected edge S2->F1")
2064+
}
2065+
2066+
if !journey.Graph.Edge(f1, s2) {
2067+
t.Errorf("Expected edge F1->S2")
2068+
}
2069+
2070+
if !journey.Graph.Edge(f1, s3) {
2071+
t.Errorf("Expected edge F1->S3")
2072+
}
2073+
2074+
if !journey.Graph.Edge(s3, f2) {
2075+
t.Errorf("Expected edge S3->F2")
2076+
}
2077+
2078+
if !journey.Graph.Edge(a1, a2) {
2079+
t.Errorf("Expected edge A1->A2")
2080+
}
2081+
2082+
if !journey.Graph.Edge(a2, a3) {
2083+
t.Errorf("Expected edge A2->A3")
2084+
}
2085+
2086+
if !journey.Graph.Edge(a3, s2) {
2087+
t.Errorf("Expected edge A3->S2")
20562088
}
20572089
}

backend/api/journey/ios.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,25 @@ func (j *JourneyiOS) buildGraph() {
164164
lastParent = i
165165
}
166166

167+
// ScreenView nodes should also update lastParent to enable
168+
// sequential chaining (ScreenView1 -> ScreenView2 -> ScreenView3)
169+
if currNode.IsScreenView {
170+
lastParent = i
171+
172+
// if going from screen view to view controller/swiftui, we find the
173+
// last view controller and set that as the next node's parent
174+
if nextNode.IsViewController || nextNode.IsSwiftUI {
175+
parentNode := j.GetLastViewController(&currNode)
176+
if parentNode != nil {
177+
lastParent = parentNode.ID
178+
} else {
179+
// did not find a parent view controller
180+
// will not create an edge
181+
continue
182+
}
183+
}
184+
}
185+
167186
vkey := j.Nodes[lastParent].Name
168187
wkey := nextNode.Name
169188
v := j.nodelut[vkey]
@@ -354,6 +373,26 @@ func (j JourneyiOS) GetLastView(node *NodeiOS) (parent *NodeiOS) {
354373
return
355374
}
356375

376+
// GetLastViewController finds the last ViewController or SwiftUI
377+
// node by traversing towards start direction, excluding ScreenViews.
378+
func (j JourneyiOS) GetLastViewController(node *NodeiOS) (parent *NodeiOS) {
379+
c := node.ID
380+
381+
for {
382+
c--
383+
384+
if c < 0 {
385+
break
386+
}
387+
388+
if j.Nodes[c].IsViewController || j.Nodes[c].IsSwiftUI {
389+
return &j.Nodes[c]
390+
}
391+
}
392+
393+
return
394+
}
395+
357396
// NewJourneyiOS creates a journey graph object
358397
// from a list of iOS specific events.
359398
func NewJourneyiOS(events []event.EventField, opts *Options) (journey *JourneyiOS) {

0 commit comments

Comments
 (0)