Skip to content

Commit d4db75f

Browse files
authored
Make Analytics environment checks warnings instead of errors (#3836)
1 parent 2be43ea commit d4db75f

File tree

6 files changed

+214
-112
lines changed

6 files changed

+214
-112
lines changed

.changeset/lucky-squids-explode.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/analytics': minor
3+
---
4+
5+
Analytics now warns instead of throwing if it detects a browser environment where analytics does not work.

packages/analytics/index.test.ts

Lines changed: 122 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { findGtagScriptOnPage } from './src/helpers';
3535
import { removeGtagScript } from './testing/gtag-script-util';
3636
import { Deferred } from '@firebase/util';
3737
import { AnalyticsError } from './src/errors';
38+
import { FirebaseInstallations } from '@firebase/installations-types';
3839

3940
let analyticsInstance: FirebaseAnalytics = {} as FirebaseAnalytics;
4041
const fakeMeasurementId = 'abcd-efgh';
@@ -44,6 +45,15 @@ const customGtagName = 'customGtag';
4445
const customDataLayerName = 'customDataLayer';
4546
let clock: sinon.SinonFakeTimers;
4647

48+
// Fake indexedDB.open() request
49+
let fakeRequest = {
50+
onsuccess: () => {},
51+
result: {
52+
close: () => {}
53+
}
54+
};
55+
let idbOpenStub = stub();
56+
4757
function stubFetch(status: number, body: object): void {
4858
fetchStub = stub(window, 'fetch');
4959
const mockResponse = new Response(JSON.stringify(body), {
@@ -52,6 +62,18 @@ function stubFetch(status: number, body: object): void {
5262
fetchStub.returns(Promise.resolve(mockResponse));
5363
}
5464

65+
// Stub indexedDB.open() because sinon's clock does not know
66+
// how to wait for the real indexedDB callbacks to resolve.
67+
function stubIdbOpen(): void {
68+
(fakeRequest = {
69+
onsuccess: () => {},
70+
result: {
71+
close: () => {}
72+
}
73+
}),
74+
(idbOpenStub = stub(indexedDB, 'open').returns(fakeRequest as any));
75+
}
76+
5577
describe('FirebaseAnalytics instance tests', () => {
5678
describe('Initialization', () => {
5779
beforeEach(() => resetGlobalVars());
@@ -83,65 +105,6 @@ describe('FirebaseAnalytics instance tests', () => {
83105
);
84106
warnStub.restore();
85107
});
86-
it('Throws if cookies are not enabled', () => {
87-
const cookieStub = stub(navigator, 'cookieEnabled').value(false);
88-
const app = getFakeApp({
89-
appId: fakeAppParams.appId,
90-
apiKey: fakeAppParams.apiKey
91-
});
92-
const installations = getFakeInstallations();
93-
expect(() => analyticsFactory(app, installations)).to.throw(
94-
AnalyticsError.COOKIES_NOT_ENABLED
95-
);
96-
cookieStub.restore();
97-
});
98-
it('Throws if browser extension environment', () => {
99-
window.chrome = { runtime: { id: 'blah' } };
100-
const app = getFakeApp({
101-
appId: fakeAppParams.appId,
102-
apiKey: fakeAppParams.apiKey
103-
});
104-
const installations = getFakeInstallations();
105-
expect(() => analyticsFactory(app, installations)).to.throw(
106-
AnalyticsError.INVALID_ANALYTICS_CONTEXT
107-
);
108-
window.chrome = undefined;
109-
});
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 });
125-
const warnStub = stub(console, 'warn');
126-
const idbOpenStub = stub(indexedDB, 'open').throws(
127-
'idb open throw message'
128-
);
129-
const app = getFakeApp({
130-
appId: fakeAppParams.appId,
131-
apiKey: fakeAppParams.apiKey
132-
});
133-
const installations = getFakeInstallations();
134-
analyticsFactory(app, installations);
135-
await clock.runAllAsync();
136-
expect(warnStub.args[0][1]).to.include(
137-
AnalyticsError.INVALID_INDEXED_DB_CONTEXT
138-
);
139-
expect(warnStub.args[0][1]).to.include('idb open throw message');
140-
warnStub.restore();
141-
idbOpenStub.restore();
142-
fetchStub.restore();
143-
clock.restore();
144-
});
145108
it('Throws if creating an instance with already-used appId', () => {
146109
const app = getFakeApp(fakeAppParams);
147110
const installations = getFakeInstallations();
@@ -166,6 +129,7 @@ describe('FirebaseAnalytics instance tests', () => {
166129
window['gtag'] = gtagStub;
167130
window['dataLayer'] = [];
168131
stubFetch(200, { measurementId: fakeMeasurementId });
132+
stubIdbOpen();
169133
analyticsInstance = analyticsFactory(app, installations);
170134
});
171135
after(() => {
@@ -174,6 +138,7 @@ describe('FirebaseAnalytics instance tests', () => {
174138
removeGtagScript();
175139
fetchStub.restore();
176140
clock.restore();
141+
idbOpenStub.restore();
177142
});
178143
it('Contains reference to parent app', () => {
179144
expect(analyticsInstance.app).to.equal(app);
@@ -182,6 +147,8 @@ describe('FirebaseAnalytics instance tests', () => {
182147
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
183148
currency: 'USD'
184149
});
150+
// Successfully resolves fake IDB open request.
151+
fakeRequest.onsuccess();
185152
// Clear promise chain started by logEvent.
186153
await clock.runAllAsync();
187154
expect(gtagStub).to.have.been.calledWith('js');
@@ -219,6 +186,94 @@ describe('FirebaseAnalytics instance tests', () => {
219186
});
220187
});
221188

189+
describe('Standard app, mismatched environment', () => {
190+
let app: FirebaseApp = {} as FirebaseApp;
191+
let installations: FirebaseInstallations = {} as FirebaseInstallations;
192+
const gtagStub: SinonStub = stub();
193+
let fidDeferred: Deferred<void>;
194+
let warnStub: SinonStub;
195+
let cookieStub: SinonStub;
196+
beforeEach(() => {
197+
clock = useFakeTimers();
198+
resetGlobalVars();
199+
app = getFakeApp(fakeAppParams);
200+
fidDeferred = new Deferred<void>();
201+
installations = getFakeInstallations('fid-1234', () =>
202+
fidDeferred.resolve()
203+
);
204+
window['gtag'] = gtagStub;
205+
window['dataLayer'] = [];
206+
stubFetch(200, { measurementId: fakeMeasurementId });
207+
warnStub = stub(console, 'warn');
208+
stubIdbOpen();
209+
});
210+
afterEach(() => {
211+
delete window['gtag'];
212+
delete window['dataLayer'];
213+
fetchStub.restore();
214+
clock.restore();
215+
warnStub.restore();
216+
idbOpenStub.restore();
217+
gtagStub.resetHistory();
218+
});
219+
it('Warns on initialization if cookies not available', async () => {
220+
cookieStub = stub(navigator, 'cookieEnabled').value(false);
221+
analyticsInstance = analyticsFactory(app, installations);
222+
expect(warnStub.args[0][1]).to.include(
223+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
224+
);
225+
expect(warnStub.args[0][1]).to.include('Cookies');
226+
cookieStub.restore();
227+
});
228+
it('Warns on initialization if in browser extension', async () => {
229+
window.chrome = { runtime: { id: 'blah' } };
230+
analyticsInstance = analyticsFactory(app, installations);
231+
expect(warnStub.args[0][1]).to.include(
232+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
233+
);
234+
expect(warnStub.args[0][1]).to.include('browser extension');
235+
window.chrome = undefined;
236+
});
237+
it('Warns on logEvent if indexedDB API not available', async () => {
238+
const idbStub = stub(window, 'indexedDB').value(undefined);
239+
analyticsInstance = analyticsFactory(app, installations);
240+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
241+
currency: 'USD'
242+
});
243+
// Clear promise chain started by logEvent.
244+
await clock.runAllAsync();
245+
// gtag config call omits FID
246+
expect(gtagStub).to.be.calledWith('config', 'abcd-efgh', {
247+
update: true,
248+
origin: 'firebase'
249+
});
250+
expect(warnStub.args[0][1]).to.include(
251+
AnalyticsError.INDEXEDDB_UNAVAILABLE
252+
);
253+
expect(warnStub.args[0][1]).to.include('IndexedDB is not available');
254+
idbStub.restore();
255+
});
256+
it('Warns on logEvent if indexedDB.open() not allowed', async () => {
257+
idbOpenStub.restore();
258+
idbOpenStub = stub(indexedDB, 'open').throws('idb open error test');
259+
analyticsInstance = analyticsFactory(app, installations);
260+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
261+
currency: 'USD'
262+
});
263+
// Clear promise chain started by logEvent.
264+
await clock.runAllAsync();
265+
// gtag config call omits FID
266+
expect(gtagStub).to.be.calledWith('config', 'abcd-efgh', {
267+
update: true,
268+
origin: 'firebase'
269+
});
270+
expect(warnStub.args[0][1]).to.include(
271+
AnalyticsError.INDEXEDDB_UNAVAILABLE
272+
);
273+
expect(warnStub.args[0][1]).to.include('idb open error test');
274+
});
275+
});
276+
222277
describe('Page has user gtag script with custom gtag and dataLayer names', () => {
223278
let app: FirebaseApp = {} as FirebaseApp;
224279
let fidDeferred: Deferred<void>;
@@ -237,6 +292,7 @@ describe('FirebaseAnalytics instance tests', () => {
237292
dataLayerName: customDataLayerName,
238293
gtagName: customGtagName
239294
});
295+
stubIdbOpen();
240296
stubFetch(200, { measurementId: fakeMeasurementId });
241297
analyticsInstance = analyticsFactory(app, installations);
242298
});
@@ -246,11 +302,14 @@ describe('FirebaseAnalytics instance tests', () => {
246302
removeGtagScript();
247303
fetchStub.restore();
248304
clock.restore();
305+
idbOpenStub.restore();
249306
});
250307
it('Calls gtag correctly on logEvent (instance)', async () => {
251308
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
252309
currency: 'USD'
253310
});
311+
// Successfully resolves fake IDB open request.
312+
fakeRequest.onsuccess();
254313
// Clear promise chain started by logEvent.
255314
await clock.runAllAsync();
256315
expect(gtagStub).to.have.been.calledWith('js');
@@ -280,9 +339,12 @@ describe('FirebaseAnalytics instance tests', () => {
280339
const app = getFakeApp(fakeAppParams);
281340
const installations = getFakeInstallations();
282341
stubFetch(200, {});
342+
stubIdbOpen();
283343
analyticsInstance = analyticsFactory(app, installations);
284344

285345
const { initializationPromisesMap } = getGlobalVars();
346+
// Successfully resolves fake IDB open request.
347+
fakeRequest.onsuccess();
286348
await initializationPromisesMap[fakeAppParams.appId];
287349
expect(findGtagScriptOnPage()).to.not.be.null;
288350
expect(typeof window['gtag']).to.equal('function');
@@ -292,6 +354,7 @@ describe('FirebaseAnalytics instance tests', () => {
292354
delete window['dataLayer'];
293355
removeGtagScript();
294356
fetchStub.restore();
357+
idbOpenStub.restore();
295358
});
296359
});
297360
});

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: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,11 @@ export const enum AnalyticsError {
2222
ALREADY_INITIALIZED = 'already-initialized',
2323
INTEROP_COMPONENT_REG_FAILED = 'interop-component-reg-failed',
2424
INVALID_ANALYTICS_CONTEXT = 'invalid-analytics-context',
25+
INDEXEDDB_UNAVAILABLE = 'indexeddb-unavailable',
2526
FETCH_THROTTLE = 'fetch-throttle',
2627
CONFIG_FETCH_FAILED = 'config-fetch-failed',
2728
NO_API_KEY = 'no-api-key',
28-
NO_APP_ID = 'no-app-id',
29-
INDEXED_DB_UNSUPPORTED = 'indexedDB-unsupported',
30-
INVALID_INDEXED_DB_CONTEXT = 'invalid-indexedDB-context',
31-
COOKIES_NOT_ENABLED = 'cookies-not-enabled'
29+
NO_APP_ID = 'no-app-id'
3230
}
3331

3432
const ERRORS: ErrorMap<AnalyticsError> = {
@@ -42,16 +40,14 @@ const ERRORS: ErrorMap<AnalyticsError> = {
4240
'or it will have no effect.',
4341
[AnalyticsError.INTEROP_COMPONENT_REG_FAILED]:
4442
'Firebase Analytics Interop Component failed to instantiate: {$reason}',
45-
[AnalyticsError.INDEXED_DB_UNSUPPORTED]:
46-
'IndexedDB is not supported by current browswer',
47-
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]:
48-
"Environment doesn't support IndexedDB: {$errorInfo}. " +
49-
'Wrap initialization of analytics in analytics.isSupported() ' +
50-
'to prevent initialization in unsupported environments',
51-
[AnalyticsError.COOKIES_NOT_ENABLED]:
52-
'Cookies are not enabled in this browser environment. Analytics requires cookies to be enabled.',
5343
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]:
54-
'Firebase Analytics is not supported in browser extensions.',
44+
'Firebase Analytics is not supported in this environment. ' +
45+
'Wrap initialization of analytics in analytics.isSupported() ' +
46+
'to prevent initialization in unsupported environments. Details: {$errorInfo}',
47+
[AnalyticsError.INDEXEDDB_UNAVAILABLE]:
48+
'IndexedDB unavailable or restricted in this environment. ' +
49+
'Wrap initialization of analytics in analytics.isSupported() ' +
50+
'to prevent initialization in unsupported environments. Details: {$errorInfo}',
5551
[AnalyticsError.FETCH_THROTTLE]:
5652
'The config fetch request timed out while in an exponential backoff state.' +
5753
' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
@@ -62,15 +58,7 @@ const ERRORS: ErrorMap<AnalyticsError> = {
6258
'contain a valid API key.',
6359
[AnalyticsError.NO_APP_ID]:
6460
'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.'
61+
'contain a valid app ID.'
7462
};
7563

7664
interface ErrorParams {
@@ -81,7 +69,8 @@ interface ErrorParams {
8169
httpStatus: number;
8270
responseMessage: string;
8371
};
84-
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]: { errorInfo: string };
72+
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]: { errorInfo: string };
73+
[AnalyticsError.INDEXEDDB_UNAVAILABLE]: { errorInfo: string };
8574
}
8675

8776
export const ERROR_FACTORY = new ErrorFactory<AnalyticsError, ErrorParams>(

0 commit comments

Comments
 (0)