Skip to content

Commit 2cc1cc4

Browse files
authored
Using deferred Analytics with Crashlytics. (#2555)
* Using deferred Analytics with Crashlytics. * Use DeferredApi instead of supressing the warning. * Re-implement interfaces to better decouple deferred-dependant objects. * Synchronize handler registration in BreadcrumbsSource. * Used volatile for the references. * Only register handlers for `DisabledBreadcrumbSource` * Rename `AnalyticsDeferredComponents` to `AnalyticsDeferredProxy` * Add tests for Analytics deferred logic.
1 parent 8a018b7 commit 2cc1cc4

File tree

5 files changed

+258
-110
lines changed

5 files changed

+258
-110
lines changed

firebase-crashlytics/firebase-crashlytics.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,15 @@ dependencies {
5757

5858
implementation project(':encoders:firebase-encoders')
5959
implementation project(':encoders:firebase-encoders-json')
60+
implementation project(':firebase-annotations')
6061
implementation project(':firebase-common')
6162
implementation project(':firebase-components')
6263
implementation project(':transport:transport-api')
6364
implementation project(':transport:transport-runtime')
6465
implementation project(':transport:transport-backend-cct')
6566
implementation project(':firebase-installations-interop')
6667
runtimeOnly project(':firebase-installations')
67-
implementation 'com.google.firebase:firebase-measurement-connector:18.0.0'
68+
implementation 'com.google.firebase:firebase-measurement-connector:18.0.2'
6869
implementation "com.google.android.gms:play-services-tasks:17.0.0"
6970

7071
javadocClasspath 'com.google.code.findbugs:jsr305:3.0.2'
@@ -82,6 +83,7 @@ dependencies {
8283
androidTestImplementation 'androidx.test:runner:1.2.0'
8384
androidTestImplementation 'androidx.test:core:1.2.0'
8485
androidTestImplementation 'org.mockito:mockito-core:2.25.0'
86+
androidTestImplementation "com.google.truth:truth:$googleTruthVersion"
8587
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.25.0'
8688
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.0'
8789
androidTestImplementation 'com.google.protobuf:protobuf-java:2.4.1'

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/CrashlyticsAnalyticsListenerTest.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,15 @@
2323
import static org.mockito.ArgumentMatchers.any;
2424
import static org.mockito.ArgumentMatchers.anyString;
2525
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.Mockito.mock;
27+
import static org.mockito.Mockito.never;
28+
import static org.mockito.Mockito.when;
2629

2730
import android.os.Bundle;
31+
import com.google.firebase.analytics.connector.AnalyticsConnector;
32+
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
2833
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventReceiver;
34+
import com.google.firebase.crashlytics.internal.breadcrumbs.BreadcrumbSource;
2935
import org.junit.Before;
3036
import org.junit.Test;
3137
import org.mockito.ArgumentCaptor;
@@ -39,6 +45,12 @@ public class CrashlyticsAnalyticsListenerTest {
3945

4046
@Mock private AnalyticsEventReceiver crashlyticsOriginEventReceiver;
4147

48+
@Mock private AnalyticsConnector analyticsConnector;
49+
50+
@Mock private BreadcrumbSource breadcrumbSource;
51+
52+
@Mock private AnalyticsEventLogger analyticsEventLogger;
53+
4254
private CrashlyticsAnalyticsListener listener;
4355

4456
@Before
@@ -131,6 +143,49 @@ public void testNullBreadcrumbReceiverDropsEvent() {
131143
.onEvent(anyString(), any(Bundle.class));
132144
}
133145

146+
@Test
147+
public void testAnalyticsDeferredProxyDefaultObjects() {
148+
AnalyticsDeferredProxy proxy =
149+
new AnalyticsDeferredProxy(handler -> {}, breadcrumbSource, analyticsEventLogger);
150+
proxy.getAnalyticsEventLogger().logEvent("EventName", new Bundle());
151+
proxy.getDeferredBreadcrumbSource().registerBreadcrumbHandler(null);
152+
153+
Mockito.verify(breadcrumbSource).registerBreadcrumbHandler(any());
154+
Mockito.verify(analyticsEventLogger).logEvent(eq("EventName"), any());
155+
}
156+
157+
@Test
158+
public void testAnalyticsDeferredProxyAvailableButNull() {
159+
AnalyticsDeferredProxy proxy =
160+
new AnalyticsDeferredProxy(
161+
p -> p.handle(() -> analyticsConnector), breadcrumbSource, analyticsEventLogger);
162+
proxy.getAnalyticsEventLogger().logEvent("EventName", new Bundle());
163+
proxy.getDeferredBreadcrumbSource().registerBreadcrumbHandler(null);
164+
165+
// Since the connector is not setup, it should fail registering and the default mocks should be
166+
// invoked.
167+
Mockito.verify(analyticsConnector, Mockito.atLeast(1))
168+
.registerAnalyticsConnectorListener(any(), any());
169+
Mockito.verify(breadcrumbSource).registerBreadcrumbHandler(any());
170+
Mockito.verify(analyticsEventLogger).logEvent(eq("EventName"), any());
171+
}
172+
173+
@Test
174+
public void testAnalyticsDeferredProxyAvailable() {
175+
when(analyticsConnector.registerAnalyticsConnectorListener(any(), any()))
176+
.thenReturn(mock(AnalyticsConnector.AnalyticsConnectorHandle.class));
177+
178+
AnalyticsDeferredProxy proxy =
179+
new AnalyticsDeferredProxy(
180+
p -> p.handle(() -> analyticsConnector), breadcrumbSource, analyticsEventLogger);
181+
proxy.getAnalyticsEventLogger().logEvent("EventName", new Bundle());
182+
proxy.getDeferredBreadcrumbSource().registerBreadcrumbHandler(null);
183+
184+
// Mocks passed in the constructor shouldn't need to be used.
185+
Mockito.verify(breadcrumbSource, never()).registerBreadcrumbHandler(any());
186+
Mockito.verify(analyticsEventLogger, never()).logEvent(eq("EventName"), any());
187+
}
188+
134189
private static Bundle makeEventBundle(String name, Bundle params) {
135190
final Bundle extras = new Bundle();
136191
extras.putString(EVENT_NAME_KEY, name);
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2021 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;
16+
17+
import static com.google.firebase.crashlytics.FirebaseCrashlytics.APP_EXCEPTION_CALLBACK_TIMEOUT_MS;
18+
import static com.google.firebase.crashlytics.FirebaseCrashlytics.FIREBASE_CRASHLYTICS_ANALYTICS_ORIGIN;
19+
import static com.google.firebase.crashlytics.FirebaseCrashlytics.LEGACY_CRASH_ANALYTICS_ORIGIN;
20+
21+
import androidx.annotation.GuardedBy;
22+
import androidx.annotation.NonNull;
23+
import com.google.firebase.analytics.connector.AnalyticsConnector;
24+
import com.google.firebase.annotations.DeferredApi;
25+
import com.google.firebase.crashlytics.internal.Logger;
26+
import com.google.firebase.crashlytics.internal.analytics.AnalyticsEventLogger;
27+
import com.google.firebase.crashlytics.internal.analytics.BlockingAnalyticsEventLogger;
28+
import com.google.firebase.crashlytics.internal.analytics.BreadcrumbAnalyticsEventReceiver;
29+
import com.google.firebase.crashlytics.internal.analytics.CrashlyticsOriginAnalyticsEventLogger;
30+
import com.google.firebase.crashlytics.internal.analytics.UnavailableAnalyticsEventLogger;
31+
import com.google.firebase.crashlytics.internal.breadcrumbs.BreadcrumbHandler;
32+
import com.google.firebase.crashlytics.internal.breadcrumbs.BreadcrumbSource;
33+
import com.google.firebase.crashlytics.internal.breadcrumbs.DisabledBreadcrumbSource;
34+
import com.google.firebase.inject.Deferred;
35+
import java.util.ArrayList;
36+
import java.util.List;
37+
import java.util.concurrent.TimeUnit;
38+
39+
/** @hide */
40+
public class AnalyticsDeferredProxy {
41+
private final Deferred<AnalyticsConnector> analyticsConnectorDeferred;
42+
private volatile AnalyticsEventLogger analyticsEventLogger;
43+
private volatile BreadcrumbSource breadcrumbSource;
44+
45+
@GuardedBy("this")
46+
private final List<BreadcrumbHandler> breadcrumbHandlerList;
47+
48+
public AnalyticsDeferredProxy(Deferred<AnalyticsConnector> analyticsConnectorDeferred) {
49+
this(
50+
analyticsConnectorDeferred,
51+
new DisabledBreadcrumbSource(),
52+
new UnavailableAnalyticsEventLogger());
53+
}
54+
55+
public AnalyticsDeferredProxy(
56+
Deferred<AnalyticsConnector> analyticsConnectorDeferred,
57+
@NonNull BreadcrumbSource breadcrumbSource,
58+
@NonNull AnalyticsEventLogger analyticsEventLogger) {
59+
this.analyticsConnectorDeferred = analyticsConnectorDeferred;
60+
this.breadcrumbSource = breadcrumbSource;
61+
this.breadcrumbHandlerList = new ArrayList<>();
62+
this.analyticsEventLogger = analyticsEventLogger;
63+
init();
64+
}
65+
66+
public BreadcrumbSource getDeferredBreadcrumbSource() {
67+
return breadcrumbHandler -> {
68+
synchronized (this) {
69+
if (breadcrumbSource instanceof DisabledBreadcrumbSource) {
70+
breadcrumbHandlerList.add(breadcrumbHandler);
71+
}
72+
breadcrumbSource.registerBreadcrumbHandler(breadcrumbHandler);
73+
}
74+
};
75+
}
76+
77+
public AnalyticsEventLogger getAnalyticsEventLogger() {
78+
return (name, params) -> analyticsEventLogger.logEvent(name, params);
79+
}
80+
81+
private void init() {
82+
analyticsConnectorDeferred.whenAvailable(
83+
analyticsConnector -> {
84+
AnalyticsConnector connector = analyticsConnector.get();
85+
// If FA is available, create a logger to log events from the Crashlytics origin.
86+
final CrashlyticsOriginAnalyticsEventLogger directAnalyticsEventLogger =
87+
new CrashlyticsOriginAnalyticsEventLogger(connector);
88+
89+
// Create a listener to register for events coming from FA, which supplies both
90+
// breadcrumbs
91+
// as well as Crashlytics-origin events through different streams.
92+
final CrashlyticsAnalyticsListener crashlyticsAnalyticsListener =
93+
new CrashlyticsAnalyticsListener();
94+
95+
// Registering our listener with FA should return a "handle", in which case we know we've
96+
// registered successfully. Subsequent calls to register a listener will return null.
97+
final AnalyticsConnector.AnalyticsConnectorHandle analyticsConnectorHandle =
98+
subscribeToAnalyticsEvents(connector, crashlyticsAnalyticsListener);
99+
100+
if (analyticsConnectorHandle != null) {
101+
Logger.getLogger().d("Registered Firebase Analytics listener.");
102+
// Create the event receiver which will supply breadcrumb events to Crashlytics
103+
final BreadcrumbAnalyticsEventReceiver breadcrumbReceiver =
104+
new BreadcrumbAnalyticsEventReceiver();
105+
// Logging events to FA is an asynchronous operation. This logger will send events to
106+
// FA and block until FA returns the same event back to us, from the Crashlytics origin.
107+
// However, in the case that data collection has been disabled on FA, we will not
108+
// receive
109+
// the event back (it will be silently dropped), so we set up a short timeout after
110+
// which
111+
// we will assume that FA data collection is disabled and move on.
112+
final BlockingAnalyticsEventLogger blockingAnalyticsEventLogger =
113+
new BlockingAnalyticsEventLogger(
114+
directAnalyticsEventLogger,
115+
APP_EXCEPTION_CALLBACK_TIMEOUT_MS,
116+
TimeUnit.MILLISECONDS);
117+
118+
synchronized (this) {
119+
// We need to re-register every handler registered in the other receiver. These
120+
// instructions are synchronized to ensure no handler is lost when registering the new
121+
// objects.
122+
for (BreadcrumbHandler handler : breadcrumbHandlerList) {
123+
breadcrumbReceiver.registerBreadcrumbHandler(handler);
124+
}
125+
// Set the appropriate event receivers to receive events from the FA listener
126+
crashlyticsAnalyticsListener.setBreadcrumbEventReceiver(breadcrumbReceiver);
127+
crashlyticsAnalyticsListener.setCrashlyticsOriginEventReceiver(
128+
blockingAnalyticsEventLogger);
129+
130+
// Set the breadcrumb event receiver as the breadcrumb source for Crashlytics.
131+
breadcrumbSource = breadcrumbReceiver;
132+
// Set the blocking analytics event logger for Crashlytics.
133+
analyticsEventLogger = blockingAnalyticsEventLogger;
134+
}
135+
} else {
136+
Logger.getLogger()
137+
.w(
138+
"Could not register Firebase Analytics listener; a listener is already registered.");
139+
// FA is enabled, but the listener was not registered successfully.
140+
// We cannot listen for breadcrumbs. Since the default is already
141+
// `DisabledBreadcrumbSource` and `directAnalyticsEventLogger` there's nothing else to
142+
// do.
143+
}
144+
});
145+
}
146+
147+
/**
148+
* Subscribes to Analytics events.
149+
*
150+
* <p>Should only be called from within the context of `whenAvailable` in the Deferred Connector.
151+
*
152+
* @param analyticsConnector Connector to Analytics.
153+
* @param listener Crashlytics listener to subscribe to analytics.
154+
* @return The Analytics handler
155+
*/
156+
@DeferredApi
157+
private static AnalyticsConnector.AnalyticsConnectorHandle subscribeToAnalyticsEvents(
158+
@NonNull AnalyticsConnector analyticsConnector,
159+
@NonNull CrashlyticsAnalyticsListener listener) {
160+
161+
AnalyticsConnector.AnalyticsConnectorHandle handle =
162+
analyticsConnector.registerAnalyticsConnectorListener(
163+
FIREBASE_CRASHLYTICS_ANALYTICS_ORIGIN, listener);
164+
165+
if (handle == null) {
166+
Logger.getLogger()
167+
.d("Could not register AnalyticsConnectorListener with Crashlytics origin.");
168+
// Older versions of FA don't support CRASHLYTICS_ORIGIN. We can try using the old Firebase
169+
// Crash Reporting origin
170+
handle =
171+
analyticsConnector.registerAnalyticsConnectorListener(
172+
LEGACY_CRASH_ANALYTICS_ORIGIN, listener);
173+
174+
// If FA allows us to connect with the legacy origin, but not the new one, nudge customers
175+
// to update their FA version.
176+
if (handle != null) {
177+
Logger.getLogger()
178+
.w(
179+
"A new version of the Google Analytics for Firebase SDK is now available. "
180+
+ "For improved performance and compatibility with Crashlytics, please "
181+
+ "update to the latest version.");
182+
}
183+
}
184+
185+
return handle;
186+
}
187+
}

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/CrashlyticsRegistrar.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.firebase.components.ComponentRegistrar;
2222
import com.google.firebase.components.Dependency;
2323
import com.google.firebase.crashlytics.internal.CrashlyticsNativeComponent;
24+
import com.google.firebase.inject.Deferred;
2425
import com.google.firebase.installations.FirebaseInstallationsApi;
2526
import com.google.firebase.platforminfo.LibraryVersionComponent;
2627
import java.util.Arrays;
@@ -34,7 +35,7 @@ public List<Component<?>> getComponents() {
3435
Component.builder(FirebaseCrashlytics.class)
3536
.add(Dependency.required(FirebaseApp.class))
3637
.add(Dependency.required(FirebaseInstallationsApi.class))
37-
.add(Dependency.optional(AnalyticsConnector.class))
38+
.add(Dependency.deferred(AnalyticsConnector.class))
3839
.add(Dependency.optional(CrashlyticsNativeComponent.class))
3940
.factory(this::buildCrashlytics)
4041
.eagerInDefaultApp()
@@ -47,7 +48,8 @@ private FirebaseCrashlytics buildCrashlytics(ComponentContainer container) {
4748

4849
CrashlyticsNativeComponent nativeComponent = container.get(CrashlyticsNativeComponent.class);
4950

50-
AnalyticsConnector analyticsConnector = container.get(AnalyticsConnector.class);
51+
Deferred<AnalyticsConnector> analyticsConnector =
52+
container.getDeferred(AnalyticsConnector.class);
5153

5254
FirebaseInstallationsApi firebaseInstallations = container.get(FirebaseInstallationsApi.class);
5355

0 commit comments

Comments
 (0)