Skip to content

Commit b76de4f

Browse files
authored
Persist the aqs session id on new aqs session (#5274)
Persist the aqs session id on new aqs session, instead of on event. This is needed for ANRs and native crashes that can't persist on event. This also populates the aqs session id and fid in the Report. The new `CrashlyticsAppQualitySessionsStore` class is based on the existing `MetaDataStore` class but with the current session id not being final.
1 parent ecb313c commit b76de4f

File tree

14 files changed

+545
-45
lines changed

14 files changed

+545
-45
lines changed

firebase-crashlytics/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Unreleased
2+
* [feature] Include Firebase sessions with NDK crashes and ANRs.
23

34
# 18.4.1
45
* [changed] Updated `firebase-sessions` dependency to v1.0.2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.crashlytics.internal.common;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static com.google.firebase.crashlytics.internal.common.CrashlyticsAppQualitySessionsStore.readAqsSessionIdFile;
19+
import static org.mockito.ArgumentMatchers.anyString;
20+
import static org.mockito.Mockito.spy;
21+
import static org.mockito.Mockito.when;
22+
23+
import com.google.firebase.crashlytics.internal.CrashlyticsTestCase;
24+
import com.google.firebase.crashlytics.internal.persistence.FileStore;
25+
26+
public final class CrashlyticsAppQualitySessionsStoreTest extends CrashlyticsTestCase {
27+
private static final String CLOSED_SESSION = null;
28+
29+
private static final String SESSION_ID = "64e61da7023800012303a14eecd3f58d";
30+
private static final String APP_QUALITY_SESSION_ID = "79fd5d2e08ef4ea9a4f378879e53af2e";
31+
private static final String NEW_SESSION_ID = "64e61da0007500012284a14eecd3f58d";
32+
private static final String NEW_APP_QUALITY_SESSION_ID = "f7196b60000a44a092b5cc9e624f551c";
33+
34+
private CrashlyticsAppQualitySessionsStore aqsStore;
35+
private FileStore fileStore;
36+
37+
@Override
38+
protected void setUp() throws Exception {
39+
// The files created by each test case will get cleaned up in super.tearDown().
40+
fileStore = spy(new FileStore(getContext()));
41+
aqsStore = new CrashlyticsAppQualitySessionsStore(fileStore);
42+
43+
when(fileStore.getSessionFile(anyString(), anyString()))
44+
.thenAnswer(
45+
invocation -> {
46+
// The AQS file store relies on file timestamps. This sleep ensures each new file
47+
// has a unique timestamp, so the most recent file can be found deterministically.
48+
Thread.sleep(1000L);
49+
return invocation.callRealMethod();
50+
});
51+
}
52+
53+
public void testRotateAqsId_neverRotatedSessionId_doesNotPersist() {
54+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
55+
56+
// Does not create a file because there is no session to persist in.
57+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isNull();
58+
}
59+
60+
public void testRotateSessionId_neverRotatedAqsId_doesNotPersist() {
61+
aqsStore.rotateSessionId(SESSION_ID);
62+
63+
// Does not create a file because there was no aqs id to persist.
64+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isNull();
65+
}
66+
67+
public void testRotateBothIds_persists() {
68+
aqsStore.rotateSessionId(SESSION_ID);
69+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
70+
71+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
72+
}
73+
74+
public void testRotateBothIds_storesIds() {
75+
aqsStore.rotateSessionId(SESSION_ID);
76+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
77+
78+
// Delete all session files to verify the getter is reading the locally stored ids.
79+
fileStore.deleteSessionFiles(SESSION_ID);
80+
81+
assertThat(aqsStore.getAppQualitySessionId(SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
82+
}
83+
84+
public void testRotateBothIds_thenRotateAqsId_persists() {
85+
aqsStore.rotateSessionId(SESSION_ID);
86+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
87+
88+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
89+
90+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(NEW_APP_QUALITY_SESSION_ID);
91+
}
92+
93+
public void testRotateBothIds_thenRotateSessionId_persistsInNewSession() {
94+
aqsStore.rotateSessionId(SESSION_ID);
95+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
96+
97+
aqsStore.rotateSessionId(NEW_SESSION_ID);
98+
99+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
100+
assertThat(readAqsSessionIdFile(fileStore, NEW_SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
101+
}
102+
103+
public void testRotateBothIds_thenSessionId_thenAqsId_persistsInNewSessionOnly() {
104+
aqsStore.rotateSessionId(SESSION_ID);
105+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
106+
107+
aqsStore.rotateSessionId(NEW_SESSION_ID);
108+
109+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
110+
111+
// Old session still contains the old aqs id.
112+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
113+
114+
// New sessions contains the new aqs id.
115+
assertThat(readAqsSessionIdFile(fileStore, NEW_SESSION_ID))
116+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
117+
}
118+
119+
public void testRotateBothIds_thenAqsId_thenSessionId_persistsInBothSessions() {
120+
aqsStore.rotateSessionId(SESSION_ID);
121+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
122+
123+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
124+
125+
aqsStore.rotateSessionId(NEW_SESSION_ID);
126+
127+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(NEW_APP_QUALITY_SESSION_ID);
128+
assertThat(readAqsSessionIdFile(fileStore, NEW_SESSION_ID))
129+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
130+
}
131+
132+
public void testRotateBothIds_thenReadInvalidSessionId_returnsNull() {
133+
aqsStore.rotateSessionId(SESSION_ID);
134+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
135+
136+
assertThat(readAqsSessionIdFile(fileStore, "sessionDoesNotExist")).isNull();
137+
}
138+
139+
public void testRotateAqsIdWhileSessionClosed_persistsAqsIdInNewSessionOnly() {
140+
// Setup first session.
141+
aqsStore.rotateSessionId(SESSION_ID);
142+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
143+
144+
// Close the first session.
145+
aqsStore.rotateSessionId(CLOSED_SESSION);
146+
147+
// Rotate the aqs session id while Crashlytics session is closed.
148+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
149+
150+
// Start a new Crashlytics session.
151+
aqsStore.rotateSessionId(NEW_SESSION_ID);
152+
153+
// Verify the old session has the old aqs id
154+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
155+
156+
// Verify the new session has the updated aqs id.
157+
assertThat(readAqsSessionIdFile(fileStore, NEW_SESSION_ID))
158+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
159+
}
160+
161+
public void testUpdateAqsIdWhileSessionFailedToClosed_persistsNewAqsIdInBothSessions() {
162+
// Setup first session.
163+
aqsStore.rotateSessionId(SESSION_ID);
164+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
165+
166+
// Simulate failing to close the first session by not closing it.
167+
168+
// Update the aqs session id while Crashlytics session failed to closed.
169+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
170+
171+
// Start a new Crashlytics session.
172+
aqsStore.rotateSessionId(NEW_SESSION_ID);
173+
174+
// Verify the old session has the new aqs id since it failed to close.
175+
assertThat(readAqsSessionIdFile(fileStore, SESSION_ID)).isEqualTo(NEW_APP_QUALITY_SESSION_ID);
176+
177+
// Verify the new session has the updated aqs id.
178+
assertThat(readAqsSessionIdFile(fileStore, NEW_SESSION_ID))
179+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
180+
}
181+
182+
public void testGetAppQualitySessionId_manyAqsIdRotations_returnsLatestAqsIdPerSession() {
183+
// Open the first Crashlytics session.
184+
aqsStore.rotateSessionId(SESSION_ID);
185+
186+
// Rotate the aqs id several times.
187+
aqsStore.rotateAppQualitySessionId("aqs id 1");
188+
aqsStore.rotateAppQualitySessionId("aqs id 2");
189+
aqsStore.rotateAppQualitySessionId("aqs id 3");
190+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
191+
192+
// Open a new Crashlytics session.
193+
aqsStore.rotateSessionId(NEW_SESSION_ID);
194+
195+
// Rotate the aqs id several times.
196+
aqsStore.rotateAppQualitySessionId("new aqs id 1");
197+
aqsStore.rotateAppQualitySessionId("new aqs id 2");
198+
aqsStore.rotateAppQualitySessionId("new aqs id 3");
199+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
200+
201+
// Verify the latest aqs id per session is returned.
202+
assertThat(aqsStore.getAppQualitySessionId(SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
203+
assertThat(aqsStore.getAppQualitySessionId(NEW_SESSION_ID))
204+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
205+
}
206+
207+
public void testGetAppQualitySessionId_manySessionIdRotations_returnsProperAqsIdForEachSession() {
208+
// Rotate the aqs id with no Crashlytics session.
209+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
210+
211+
// Rotate the Crashlytics id several times.
212+
aqsStore.rotateSessionId("session id 1");
213+
aqsStore.rotateSessionId("session id 2");
214+
aqsStore.rotateSessionId("session id 3");
215+
aqsStore.rotateSessionId(SESSION_ID);
216+
217+
// Rotate the aqs session id.
218+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
219+
220+
// Update the aqs id several times.
221+
aqsStore.rotateSessionId("new session id 1");
222+
aqsStore.rotateSessionId("new session id 2");
223+
aqsStore.rotateSessionId("new session id 3");
224+
aqsStore.rotateSessionId(NEW_SESSION_ID);
225+
226+
// Verify the correct aqs id for each session is returned.
227+
assertThat(aqsStore.getAppQualitySessionId(SESSION_ID)).isEqualTo(NEW_APP_QUALITY_SESSION_ID);
228+
assertThat(aqsStore.getAppQualitySessionId(NEW_SESSION_ID))
229+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
230+
}
231+
232+
public void testGetAppQualitySessionId_afterRelaunch_returnsPersistedAqsId() {
233+
// Setup first session.
234+
aqsStore.rotateSessionId(SESSION_ID);
235+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
236+
237+
// Rotate the aqs id during the Crashlytics session
238+
aqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
239+
240+
// Simulate a native crash and relaunch by making a new aqs store instance.
241+
CrashlyticsAppQualitySessionsStore newAqsStore =
242+
new CrashlyticsAppQualitySessionsStore(new FileStore(getContext()));
243+
244+
assertThat(newAqsStore.getAppQualitySessionId(SESSION_ID))
245+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
246+
}
247+
248+
public void testGetAppQualitySessionId_afterRelaunch_afterRotate_returnsPersistedAqsId() {
249+
// Setup first session.
250+
aqsStore.rotateSessionId(SESSION_ID);
251+
aqsStore.rotateAppQualitySessionId(APP_QUALITY_SESSION_ID);
252+
253+
// Simulate a native crash and relaunch by making a new aqs store instance.
254+
CrashlyticsAppQualitySessionsStore newAqsStore =
255+
new CrashlyticsAppQualitySessionsStore(new FileStore(getContext()));
256+
257+
// Rotate the ids in the new launch.
258+
newAqsStore.rotateSessionId(NEW_SESSION_ID);
259+
newAqsStore.rotateAppQualitySessionId(NEW_APP_QUALITY_SESSION_ID);
260+
261+
// Verify the old session still persisted the old aqs id.
262+
assertThat(newAqsStore.getAppQualitySessionId(SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
263+
}
264+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.crashlytics.internal.common;
16+
17+
import static com.google.common.truth.Truth.assertThat;
18+
import static org.mockito.Mockito.mock;
19+
20+
import androidx.annotation.NonNull;
21+
import com.google.firebase.crashlytics.internal.CrashlyticsTestCase;
22+
import com.google.firebase.crashlytics.internal.persistence.FileStore;
23+
import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails;
24+
25+
public final class CrashlyticsAppQualitySessionsSubscriberTest extends CrashlyticsTestCase {
26+
private static final String SESSION_ID = "64e61da7023800012303a14eecd3f58d";
27+
private static final String APP_QUALITY_SESSION_ID = "79fd5d2e08ef4ea9a4f378879e53af2e";
28+
private static final String NEW_SESSION_ID = "64e61da0007500012284a14eecd3f58d";
29+
private static final String NEW_APP_QUALITY_SESSION_ID = "f7196b60000a44a092b5cc9e624f551c";
30+
31+
private CrashlyticsAppQualitySessionsSubscriber aqsSubscriber;
32+
33+
@Override
34+
protected void setUp() throws Exception {
35+
// The files created by each test case will get cleaned up in super.tearDown().
36+
aqsSubscriber =
37+
new CrashlyticsAppQualitySessionsSubscriber(
38+
mock(DataCollectionArbiter.class), new FileStore(getContext()));
39+
}
40+
41+
public void testGetAppQualitySessionId_returnsLatestAqsIdForSession() {
42+
aqsSubscriber.setSessionId(SESSION_ID);
43+
44+
aqsSubscriber.onSessionChanged(createSessionDetails("aqs id 1"));
45+
aqsSubscriber.onSessionChanged(createSessionDetails("aqs id 2"));
46+
aqsSubscriber.onSessionChanged(createSessionDetails("aqs id 3"));
47+
aqsSubscriber.onSessionChanged(createSessionDetails(APP_QUALITY_SESSION_ID));
48+
49+
assertThat(aqsSubscriber.getAppQualitySessionId(SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
50+
}
51+
52+
public void testGetAppQualitySessionId_returnsCorrectAqsIdForEachSession() {
53+
String session_id_1 = "session id 1";
54+
String session_id_2 = "session id 2";
55+
String session_id_3 = "session id 3";
56+
String new_session_id_1 = "new session id 1";
57+
String new_session_id_2 = "new session id 2";
58+
String new_session_id_3 = "new session id 3";
59+
60+
aqsSubscriber.onSessionChanged(createSessionDetails(APP_QUALITY_SESSION_ID));
61+
62+
// Rotate the session id multiple times for a single aqs id.
63+
aqsSubscriber.setSessionId(session_id_1);
64+
aqsSubscriber.setSessionId(session_id_2);
65+
aqsSubscriber.setSessionId(session_id_3);
66+
aqsSubscriber.setSessionId(SESSION_ID);
67+
68+
// Close the session.
69+
aqsSubscriber.setSessionId(null);
70+
71+
// Rotate the aqs id.
72+
aqsSubscriber.onSessionChanged(createSessionDetails(NEW_APP_QUALITY_SESSION_ID));
73+
74+
// Rotate the session id multiple times again for the rotated aqs id.
75+
aqsSubscriber.setSessionId(new_session_id_1);
76+
aqsSubscriber.setSessionId(new_session_id_2);
77+
aqsSubscriber.setSessionId(new_session_id_3);
78+
aqsSubscriber.setSessionId(NEW_SESSION_ID);
79+
80+
assertThat(aqsSubscriber.getAppQualitySessionId(session_id_1))
81+
.isEqualTo(APP_QUALITY_SESSION_ID);
82+
assertThat(aqsSubscriber.getAppQualitySessionId(session_id_2))
83+
.isEqualTo(APP_QUALITY_SESSION_ID);
84+
assertThat(aqsSubscriber.getAppQualitySessionId(session_id_3))
85+
.isEqualTo(APP_QUALITY_SESSION_ID);
86+
87+
assertThat(aqsSubscriber.getAppQualitySessionId(SESSION_ID)).isEqualTo(APP_QUALITY_SESSION_ID);
88+
89+
assertThat(aqsSubscriber.getAppQualitySessionId(new_session_id_1))
90+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
91+
assertThat(aqsSubscriber.getAppQualitySessionId(new_session_id_2))
92+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
93+
assertThat(aqsSubscriber.getAppQualitySessionId(new_session_id_3))
94+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
95+
96+
assertThat(aqsSubscriber.getAppQualitySessionId(NEW_SESSION_ID))
97+
.isEqualTo(NEW_APP_QUALITY_SESSION_ID);
98+
}
99+
100+
private static SessionDetails createSessionDetails(@NonNull String appQualitySessionId) {
101+
return new SessionDetails(appQualitySessionId);
102+
}
103+
}

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CrashlyticsControllerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ public CrashlyticsController build() {
163163
logFileManager,
164164
sessionReportingCoordinator,
165165
nativeComponent,
166-
analyticsEventLogger);
166+
analyticsEventLogger,
167+
mock(CrashlyticsAppQualitySessionsSubscriber.class));
167168
return controller;
168169
}
169170
}

0 commit comments

Comments
 (0)