Skip to content

Commit 857aa82

Browse files
committed
Make environment checks warnings instead of errors
1 parent e55e7c6 commit 857aa82

File tree

6 files changed

+196
-65
lines changed

6 files changed

+196
-65
lines changed

packages/analytics/index.test.ts

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ const customGtagName = 'customGtag';
4444
const customDataLayerName = 'customDataLayer';
4545
let clock: sinon.SinonFakeTimers;
4646

47+
// Fake indexedDB.open() request
48+
let fakeRequest = {
49+
onsuccess: () => {},
50+
result: {
51+
close: () => {}
52+
}
53+
};
54+
let idbOpenStub = stub();
55+
4756
function stubFetch(status: number, body: object): void {
4857
fetchStub = stub(window, 'fetch');
4958
const mockResponse = new Response(JSON.stringify(body), {
@@ -52,6 +61,18 @@ function stubFetch(status: number, body: object): void {
5261
fetchStub.returns(Promise.resolve(mockResponse));
5362
}
5463

64+
// Stub indexedDB.open() because sinon's clock does not know
65+
// how to wait for the real indexedDB callbacks to resolve.
66+
function stubIdbOpen(): void {
67+
(fakeRequest = {
68+
onsuccess: () => {},
69+
result: {
70+
close: () => {}
71+
}
72+
}),
73+
(idbOpenStub = stub(indexedDB, 'open').returns(fakeRequest as any));
74+
}
75+
5576
describe('FirebaseAnalytics instance tests', () => {
5677
describe('Initialization', () => {
5778
beforeEach(() => resetGlobalVars());
@@ -83,64 +104,50 @@ describe('FirebaseAnalytics instance tests', () => {
83104
);
84105
warnStub.restore();
85106
});
86-
it('Throws if cookies are not enabled', () => {
107+
it('Warns if cookies are not enabled', () => {
108+
const warnStub = stub(console, 'warn');
87109
const cookieStub = stub(navigator, 'cookieEnabled').value(false);
88110
const app = getFakeApp({
89111
appId: fakeAppParams.appId,
90112
apiKey: fakeAppParams.apiKey
91113
});
92114
const installations = getFakeInstallations();
93-
expect(() => analyticsFactory(app, installations)).to.throw(
115+
analyticsFactory(app, installations);
116+
expect(warnStub.args[0][1]).to.include(
94117
AnalyticsError.COOKIES_NOT_ENABLED
95118
);
119+
warnStub.restore();
96120
cookieStub.restore();
97121
});
98-
it('Throws if browser extension environment', () => {
122+
it('Warns if browser extension environment', () => {
123+
const warnStub = stub(console, 'warn');
99124
window.chrome = { runtime: { id: 'blah' } };
100125
const app = getFakeApp({
101126
appId: fakeAppParams.appId,
102127
apiKey: fakeAppParams.apiKey
103128
});
104129
const installations = getFakeInstallations();
105-
expect(() => analyticsFactory(app, installations)).to.throw(
130+
analyticsFactory(app, installations);
131+
expect(warnStub.args[0][1]).to.include(
106132
AnalyticsError.INVALID_ANALYTICS_CONTEXT
107133
);
134+
warnStub.restore();
108135
window.chrome = undefined;
109136
});
110-
it('Throws if indexedDB does not exist', () => {
111-
const idbStub = stub(window, 'indexedDB').value(undefined);
112-
const app = getFakeApp({
113-
appId: fakeAppParams.appId,
114-
apiKey: fakeAppParams.apiKey
115-
});
116-
const installations = getFakeInstallations();
117-
expect(() => analyticsFactory(app, installations)).to.throw(
118-
AnalyticsError.INDEXED_DB_UNSUPPORTED
119-
);
120-
idbStub.restore();
121-
});
122-
it('Warns eventually if indexedDB.open() does not work', async () => {
123-
clock = useFakeTimers();
124-
stubFetch(200, { measurementId: fakeMeasurementId });
137+
it('Warns if indexedDB does not exist', () => {
125138
const warnStub = stub(console, 'warn');
126-
const idbOpenStub = stub(indexedDB, 'open').throws(
127-
'idb open throw message'
128-
);
139+
const idbStub = stub(window, 'indexedDB').value(undefined);
129140
const app = getFakeApp({
130141
appId: fakeAppParams.appId,
131142
apiKey: fakeAppParams.apiKey
132143
});
133144
const installations = getFakeInstallations();
134145
analyticsFactory(app, installations);
135-
await clock.runAllAsync();
136146
expect(warnStub.args[0][1]).to.include(
137-
AnalyticsError.INVALID_INDEXED_DB_CONTEXT
147+
AnalyticsError.INDEXED_DB_UNSUPPORTED
138148
);
139-
expect(warnStub.args[0][1]).to.include('idb open throw message');
140149
warnStub.restore();
141-
idbOpenStub.restore();
142-
fetchStub.restore();
143-
clock.restore();
150+
idbStub.restore();
144151
});
145152
it('Throws if creating an instance with already-used appId', () => {
146153
const app = getFakeApp(fakeAppParams);
@@ -166,6 +173,7 @@ describe('FirebaseAnalytics instance tests', () => {
166173
window['gtag'] = gtagStub;
167174
window['dataLayer'] = [];
168175
stubFetch(200, { measurementId: fakeMeasurementId });
176+
stubIdbOpen();
169177
analyticsInstance = analyticsFactory(app, installations);
170178
});
171179
after(() => {
@@ -174,6 +182,7 @@ describe('FirebaseAnalytics instance tests', () => {
174182
removeGtagScript();
175183
fetchStub.restore();
176184
clock.restore();
185+
idbOpenStub.restore();
177186
});
178187
it('Contains reference to parent app', () => {
179188
expect(analyticsInstance.app).to.equal(app);
@@ -182,6 +191,8 @@ describe('FirebaseAnalytics instance tests', () => {
182191
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
183192
currency: 'USD'
184193
});
194+
// Successfully resolves fake IDB open request.
195+
fakeRequest.onsuccess();
185196
// Clear promise chain started by logEvent.
186197
await clock.runAllAsync();
187198
expect(gtagStub).to.have.been.calledWith('js');
@@ -219,6 +230,48 @@ describe('FirebaseAnalytics instance tests', () => {
219230
});
220231
});
221232

233+
describe('Standard app, indexedDB.open not available', () => {
234+
let app: FirebaseApp = {} as FirebaseApp;
235+
let fidDeferred: Deferred<void>;
236+
const gtagStub: SinonStub = stub();
237+
let warnStub: SinonStub;
238+
before(() => {
239+
clock = useFakeTimers();
240+
resetGlobalVars();
241+
app = getFakeApp(fakeAppParams);
242+
fidDeferred = new Deferred<void>();
243+
const installations = getFakeInstallations('fid-1234', () =>
244+
fidDeferred.resolve()
245+
);
246+
window['gtag'] = gtagStub;
247+
window['dataLayer'] = [];
248+
stubFetch(200, { measurementId: fakeMeasurementId });
249+
warnStub = stub(console, 'warn');
250+
idbOpenStub = stub(indexedDB, 'open').throws('idb open error');
251+
analyticsInstance = analyticsFactory(app, installations);
252+
});
253+
after(() => {
254+
delete window['gtag'];
255+
delete window['dataLayer'];
256+
removeGtagScript();
257+
fetchStub.restore();
258+
clock.restore();
259+
idbOpenStub.restore();
260+
warnStub.restore();
261+
});
262+
it('Does not call gtag on logEvent but does not throw', async () => {
263+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
264+
currency: 'USD'
265+
});
266+
// Clear promise chain started by logEvent.
267+
await clock.runAllAsync();
268+
expect(gtagStub).to.not.have.been.called;
269+
expect(warnStub.args[0][1]).to.include(
270+
AnalyticsError.INVALID_INDEXED_DB_CONTEXT
271+
);
272+
});
273+
});
274+
222275
describe('Page has user gtag script with custom gtag and dataLayer names', () => {
223276
let app: FirebaseApp = {} as FirebaseApp;
224277
let fidDeferred: Deferred<void>;
@@ -237,6 +290,7 @@ describe('FirebaseAnalytics instance tests', () => {
237290
dataLayerName: customDataLayerName,
238291
gtagName: customGtagName
239292
});
293+
stubIdbOpen();
240294
stubFetch(200, { measurementId: fakeMeasurementId });
241295
analyticsInstance = analyticsFactory(app, installations);
242296
});
@@ -246,11 +300,14 @@ describe('FirebaseAnalytics instance tests', () => {
246300
removeGtagScript();
247301
fetchStub.restore();
248302
clock.restore();
303+
idbOpenStub.restore();
249304
});
250305
it('Calls gtag correctly on logEvent (instance)', async () => {
251306
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
252307
currency: 'USD'
253308
});
309+
// Successfully resolves fake IDB open request.
310+
fakeRequest.onsuccess();
254311
// Clear promise chain started by logEvent.
255312
await clock.runAllAsync();
256313
expect(gtagStub).to.have.been.calledWith('js');
@@ -280,9 +337,12 @@ describe('FirebaseAnalytics instance tests', () => {
280337
const app = getFakeApp(fakeAppParams);
281338
const installations = getFakeInstallations();
282339
stubFetch(200, {});
340+
stubIdbOpen();
283341
analyticsInstance = analyticsFactory(app, installations);
284342

285343
const { initializationPromisesMap } = getGlobalVars();
344+
// Successfully resolves fake IDB open request.
345+
fakeRequest.onsuccess();
286346
await initializationPromisesMap[fakeAppParams.appId];
287347
expect(findGtagScriptOnPage()).to.not.be.null;
288348
expect(typeof window['gtag']).to.equal('function');
@@ -292,6 +352,7 @@ describe('FirebaseAnalytics instance tests', () => {
292352
delete window['dataLayer'];
293353
removeGtagScript();
294354
fetchStub.restore();
355+
idbOpenStub.restore();
295356
});
296357
});
297358
});

packages/analytics/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ declare global {
5050
* Type constant for Firebase Analytics.
5151
*/
5252
const ANALYTICS_TYPE = 'analytics';
53+
5354
export function registerAnalytics(instance: _FirebaseNamespace): void {
5455
instance.INTERNAL.registerComponent(
5556
new Component(

packages/analytics/src/errors.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const ERRORS: ErrorMap<AnalyticsError> = {
4343
[AnalyticsError.INTEROP_COMPONENT_REG_FAILED]:
4444
'Firebase Analytics Interop Component failed to instantiate: {$reason}',
4545
[AnalyticsError.INDEXED_DB_UNSUPPORTED]:
46-
'IndexedDB is not supported by current browswer',
46+
'IndexedDB is not supported by current browser',
4747
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]:
4848
"Environment doesn't support IndexedDB: {$errorInfo}. " +
4949
'Wrap initialization of analytics in analytics.isSupported() ' +
@@ -62,15 +62,7 @@ const ERRORS: ErrorMap<AnalyticsError> = {
6262
'contain a valid API key.',
6363
[AnalyticsError.NO_APP_ID]:
6464
'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
65-
'contain a valid app ID.',
66-
[AnalyticsError.INDEXED_DB_UNSUPPORTED]:
67-
'IndexedDB is not supported by current browswer',
68-
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]:
69-
"Environment doesn't support IndexedDB: {$errorInfo}. " +
70-
'Wrap initialization of analytics in analytics.isSupported() ' +
71-
'to prevent initialization in unsupported environments',
72-
[AnalyticsError.COOKIES_NOT_ENABLED]:
73-
'Cookies are not enabled in this browser environment. Analytics requires cookies to be enabled.'
65+
'contain a valid app ID.'
7466
};
7567

7668
interface ErrorParams {

packages/analytics/src/factory.ts

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ import { FirebaseApp } from '@firebase/app-types';
4040
import { FirebaseInstallations } from '@firebase/installations-types';
4141
import {
4242
isIndexedDBAvailable,
43-
validateIndexedDBOpenable,
4443
areCookiesEnabled,
45-
isBrowserExtension
44+
isBrowserExtension,
45+
validateIndexedDBOpenable
4646
} from '@firebase/util';
4747
import { initializeIds } from './initialize-ids';
4848
import { logger } from './logger';
@@ -153,29 +153,44 @@ export function settings(options: SettingsOptions): void {
153153
}
154154
}
155155

156+
function logWarningFromCode(errorCode: AnalyticsError): void {
157+
const error = ERROR_FACTORY.create(errorCode);
158+
logger.warn(error.message);
159+
}
160+
156161
export function factory(
157162
app: FirebaseApp,
158163
installations: FirebaseInstallations
159164
): FirebaseAnalytics {
165+
const emptyAnalyticsInstance: FirebaseAnalyticsInternal = {
166+
app,
167+
// Public methods return void for API simplicity and to better match gtag,
168+
// while internal implementations return promises.
169+
logEvent: () => {},
170+
setCurrentScreen: () => {},
171+
setUserId: () => {},
172+
setUserProperties: () => {},
173+
setAnalyticsCollectionEnabled: () => {},
174+
INTERNAL: {
175+
delete: (): Promise<void> => {
176+
return Promise.resolve();
177+
}
178+
}
179+
};
180+
160181
if (isBrowserExtension()) {
161-
throw ERROR_FACTORY.create(AnalyticsError.INVALID_ANALYTICS_CONTEXT);
182+
logWarningFromCode(AnalyticsError.INVALID_ANALYTICS_CONTEXT);
183+
return emptyAnalyticsInstance;
162184
}
163185
if (!areCookiesEnabled()) {
164-
throw ERROR_FACTORY.create(AnalyticsError.COOKIES_NOT_ENABLED);
186+
logWarningFromCode(AnalyticsError.COOKIES_NOT_ENABLED);
187+
return emptyAnalyticsInstance;
165188
}
166189
if (!isIndexedDBAvailable()) {
167-
throw ERROR_FACTORY.create(AnalyticsError.INDEXED_DB_UNSUPPORTED);
190+
logWarningFromCode(AnalyticsError.INDEXED_DB_UNSUPPORTED);
191+
return emptyAnalyticsInstance;
168192
}
169-
// Async but non-blocking.
170-
validateIndexedDBOpenable().catch(error => {
171-
const analyticsError = ERROR_FACTORY.create(
172-
AnalyticsError.INVALID_INDEXED_DB_CONTEXT,
173-
{
174-
errorInfo: error
175-
}
176-
);
177-
logger.warn(analyticsError.message);
178-
});
193+
179194
const appId = app.options.appId;
180195
if (!appId) {
181196
throw ERROR_FACTORY.create(AnalyticsError.NO_APP_ID);
@@ -221,13 +236,21 @@ export function factory(
221236
}
222237
// Async but non-blocking.
223238
// This map reflects the completion state of all promises for each appId.
224-
initializationPromisesMap[appId] = initializeIds(
225-
app,
226-
dynamicConfigPromisesList,
227-
measurementIdToAppId,
228-
installations,
229-
gtagCoreFunction
230-
);
239+
initializationPromisesMap[appId] = validateIndexedDBOpenable()
240+
.then(() =>
241+
initializeIds(
242+
app,
243+
dynamicConfigPromisesList,
244+
measurementIdToAppId,
245+
installations,
246+
gtagCoreFunction
247+
)
248+
)
249+
.catch(e => {
250+
throw ERROR_FACTORY.create(AnalyticsError.INVALID_INDEXED_DB_CONTEXT, {
251+
errorInfo: e
252+
});
253+
});
231254

232255
const analyticsInstance: FirebaseAnalyticsInternal = {
233256
app,

0 commit comments

Comments
 (0)