Skip to content

Commit f13a8c8

Browse files
committed
Consolidate checking into one function
1 parent 474b307 commit f13a8c8

File tree

5 files changed

+113
-118
lines changed

5 files changed

+113
-118
lines changed

packages/analytics/index.test.ts

Lines changed: 65 additions & 56 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';
@@ -104,51 +105,6 @@ describe('FirebaseAnalytics instance tests', () => {
104105
);
105106
warnStub.restore();
106107
});
107-
it('Warns if cookies are not enabled', () => {
108-
const warnStub = stub(console, 'warn');
109-
const cookieStub = stub(navigator, 'cookieEnabled').value(false);
110-
const app = getFakeApp({
111-
appId: fakeAppParams.appId,
112-
apiKey: fakeAppParams.apiKey
113-
});
114-
const installations = getFakeInstallations();
115-
analyticsFactory(app, installations);
116-
expect(warnStub.args[0][1]).to.include(
117-
AnalyticsError.COOKIES_NOT_ENABLED
118-
);
119-
warnStub.restore();
120-
cookieStub.restore();
121-
});
122-
it('Warns if browser extension environment', () => {
123-
const warnStub = stub(console, 'warn');
124-
window.chrome = { runtime: { id: 'blah' } };
125-
const app = getFakeApp({
126-
appId: fakeAppParams.appId,
127-
apiKey: fakeAppParams.apiKey
128-
});
129-
const installations = getFakeInstallations();
130-
analyticsFactory(app, installations);
131-
expect(warnStub.args[0][1]).to.include(
132-
AnalyticsError.INVALID_ANALYTICS_CONTEXT
133-
);
134-
warnStub.restore();
135-
window.chrome = undefined;
136-
});
137-
it('Warns if indexedDB does not exist', () => {
138-
const warnStub = stub(console, 'warn');
139-
const idbStub = stub(window, 'indexedDB').value(undefined);
140-
const app = getFakeApp({
141-
appId: fakeAppParams.appId,
142-
apiKey: fakeAppParams.apiKey
143-
});
144-
const installations = getFakeInstallations();
145-
analyticsFactory(app, installations);
146-
expect(warnStub.args[0][1]).to.include(
147-
AnalyticsError.INDEXED_DB_UNSUPPORTED
148-
);
149-
warnStub.restore();
150-
idbStub.restore();
151-
});
152108
it('Throws if creating an instance with already-used appId', () => {
153109
const app = getFakeApp(fakeAppParams);
154110
const installations = getFakeInstallations();
@@ -230,45 +186,98 @@ describe('FirebaseAnalytics instance tests', () => {
230186
});
231187
});
232188

233-
describe('Standard app, indexedDB.open not available', () => {
189+
describe('Standard app, mismatched environment', () => {
234190
let app: FirebaseApp = {} as FirebaseApp;
235-
let fidDeferred: Deferred<void>;
191+
let installations: FirebaseInstallations = {} as FirebaseInstallations;
236192
const gtagStub: SinonStub = stub();
193+
let fidDeferred: Deferred<void>;
237194
let warnStub: SinonStub;
238-
before(() => {
195+
let cookieStub: SinonStub;
196+
beforeEach(() => {
239197
clock = useFakeTimers();
240198
resetGlobalVars();
241199
app = getFakeApp(fakeAppParams);
242200
fidDeferred = new Deferred<void>();
243-
const installations = getFakeInstallations('fid-1234', () =>
201+
installations = getFakeInstallations('fid-1234', () =>
244202
fidDeferred.resolve()
245203
);
246204
window['gtag'] = gtagStub;
247205
window['dataLayer'] = [];
248206
stubFetch(200, { measurementId: fakeMeasurementId });
249207
warnStub = stub(console, 'warn');
250-
idbOpenStub = stub(indexedDB, 'open').throws('idb open error');
251-
analyticsInstance = analyticsFactory(app, installations);
208+
stubIdbOpen();
252209
});
253-
after(() => {
210+
afterEach(() => {
254211
delete window['gtag'];
255212
delete window['dataLayer'];
256-
removeGtagScript();
257213
fetchStub.restore();
258214
clock.restore();
259-
idbOpenStub.restore();
260215
warnStub.restore();
216+
idbOpenStub.restore();
261217
});
262-
it('Does not call gtag on logEvent but does not throw', async () => {
218+
it('Warns on logEvent if cookies not available', async () => {
219+
cookieStub = stub(navigator, 'cookieEnabled').value(false);
220+
analyticsInstance = analyticsFactory(app, installations);
263221
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
264222
currency: 'USD'
265223
});
224+
// Successfully resolves fake IDB open request.
225+
fakeRequest.onsuccess();
266226
// Clear promise chain started by logEvent.
267227
await clock.runAllAsync();
268228
expect(gtagStub).to.not.have.been.called;
269229
expect(warnStub.args[0][1]).to.include(
270-
AnalyticsError.INVALID_INDEXED_DB_CONTEXT
230+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
231+
);
232+
expect(warnStub.args[0][1]).to.include('Cookies');
233+
cookieStub.restore();
234+
});
235+
it('Warns on logEvent if in browser extension', async () => {
236+
window.chrome = { runtime: { id: 'blah' } };
237+
analyticsInstance = analyticsFactory(app, installations);
238+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
239+
currency: 'USD'
240+
});
241+
// Successfully resolves fake IDB open request.
242+
fakeRequest.onsuccess();
243+
// Clear promise chain started by logEvent.
244+
await clock.runAllAsync();
245+
expect(gtagStub).to.not.have.been.called;
246+
expect(warnStub.args[0][1]).to.include(
247+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
248+
);
249+
expect(warnStub.args[0][1]).to.include('browser extension');
250+
window.chrome = undefined;
251+
});
252+
it('Warns on logEvent if indexedDB API not available', async () => {
253+
const idbStub = stub(window, 'indexedDB').value(undefined);
254+
analyticsInstance = analyticsFactory(app, installations);
255+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
256+
currency: 'USD'
257+
});
258+
// Clear promise chain started by logEvent.
259+
await clock.runAllAsync();
260+
expect(gtagStub).to.not.have.been.called;
261+
expect(warnStub.args[0][1]).to.include(
262+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
263+
);
264+
expect(warnStub.args[0][1]).to.include('IndexedDB is not available');
265+
idbStub.restore();
266+
});
267+
it('Warns on logEvent if indexedDB.open() not allowed', async () => {
268+
idbOpenStub.restore();
269+
idbOpenStub = stub(indexedDB, 'open').throws('idb open error test');
270+
analyticsInstance = analyticsFactory(app, installations);
271+
analyticsInstance.logEvent(EventName.ADD_PAYMENT_INFO, {
272+
currency: 'USD'
273+
});
274+
// Clear promise chain started by logEvent.
275+
await clock.runAllAsync();
276+
expect(gtagStub).to.not.have.been.called;
277+
expect(warnStub.args[0][1]).to.include(
278+
AnalyticsError.INVALID_ANALYTICS_CONTEXT
271279
);
280+
expect(warnStub.args[0][1]).to.include('idb open error test');
272281
});
273282
});
274283

packages/analytics/src/errors.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ export const enum AnalyticsError {
2525
FETCH_THROTTLE = 'fetch-throttle',
2626
CONFIG_FETCH_FAILED = 'config-fetch-failed',
2727
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'
28+
NO_APP_ID = 'no-app-id'
3229
}
3330

3431
const ERRORS: ErrorMap<AnalyticsError> = {
@@ -42,16 +39,10 @@ const ERRORS: ErrorMap<AnalyticsError> = {
4239
'or it will have no effect.',
4340
[AnalyticsError.INTEROP_COMPONENT_REG_FAILED]:
4441
'Firebase Analytics Interop Component failed to instantiate: {$reason}',
45-
[AnalyticsError.INDEXED_DB_UNSUPPORTED]:
46-
'IndexedDB is not supported by current browser',
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.',
5342
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]:
54-
'Firebase Analytics is not supported in browser extensions.',
43+
'Firebase Analytics is not supported in this environment. ' +
44+
'Wrap initialization of analytics in analytics.isSupported() ' +
45+
'to prevent initialization in unsupported environments. Details: {$errorInfo}',
5546
[AnalyticsError.FETCH_THROTTLE]:
5647
'The config fetch request timed out while in an exponential backoff state.' +
5748
' Unix timestamp in milliseconds when fetch request throttling ends: {$throttleEndTimeMillis}.',
@@ -73,7 +64,7 @@ interface ErrorParams {
7364
httpStatus: number;
7465
responseMessage: string;
7566
};
76-
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]: { errorInfo: string };
67+
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]: { errorInfo: string };
7768
}
7869

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

packages/analytics/src/factory.ts

Lines changed: 40 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -153,44 +153,45 @@ 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-
161-
export function factory(
162-
app: FirebaseApp,
163-
installations: FirebaseInstallations
164-
): 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-
156+
/**
157+
* Returns true if no environment mismatch is found.
158+
* If environment mismatches are found, throws an INVALID_ANALYTICS_CONTEXT
159+
* error that also lists details for each mismatch found.
160+
*/
161+
async function validateBrowserContext(): Promise<boolean> {
162+
const mismatchedEnvMessages = [];
181163
if (isBrowserExtension()) {
182-
logWarningFromCode(AnalyticsError.INVALID_ANALYTICS_CONTEXT);
183-
return emptyAnalyticsInstance;
164+
mismatchedEnvMessages.push('This is a browser extension environment.');
184165
}
185166
if (!areCookiesEnabled()) {
186-
logWarningFromCode(AnalyticsError.COOKIES_NOT_ENABLED);
187-
return emptyAnalyticsInstance;
167+
mismatchedEnvMessages.push('Cookies are not available.');
188168
}
189169
if (!isIndexedDBAvailable()) {
190-
logWarningFromCode(AnalyticsError.INDEXED_DB_UNSUPPORTED);
191-
return emptyAnalyticsInstance;
170+
mismatchedEnvMessages.push(
171+
'IndexedDB is not available in this environment.'
172+
);
173+
} else {
174+
try {
175+
await validateIndexedDBOpenable();
176+
} catch (e) {
177+
mismatchedEnvMessages.push(e);
178+
}
179+
}
180+
if (mismatchedEnvMessages.length > 0) {
181+
const details = mismatchedEnvMessages
182+
.map((message, index) => `(${index + 1}) ${message}`)
183+
.join(' ');
184+
throw ERROR_FACTORY.create(AnalyticsError.INVALID_ANALYTICS_CONTEXT, {
185+
errorInfo: details
186+
});
192187
}
188+
return true;
189+
}
193190

191+
export function factory(
192+
app: FirebaseApp,
193+
installations: FirebaseInstallations
194+
): FirebaseAnalytics {
194195
const appId = app.options.appId;
195196
if (!appId) {
196197
throw ERROR_FACTORY.create(AnalyticsError.NO_APP_ID);
@@ -236,21 +237,15 @@ export function factory(
236237
}
237238
// Async but non-blocking.
238239
// This map reflects the completion state of all promises for each appId.
239-
initializationPromisesMap[appId] = validateIndexedDBOpenable()
240-
.then(() =>
241-
initializeIds(
242-
app,
243-
dynamicConfigPromisesList,
244-
measurementIdToAppId,
245-
installations,
246-
gtagCoreFunction
247-
)
240+
initializationPromisesMap[appId] = validateBrowserContext().then(() =>
241+
initializeIds(
242+
app,
243+
dynamicConfigPromisesList,
244+
measurementIdToAppId,
245+
installations,
246+
gtagCoreFunction
248247
)
249-
.catch(e => {
250-
throw ERROR_FACTORY.create(AnalyticsError.INVALID_INDEXED_DB_CONTEXT, {
251-
errorInfo: e
252-
});
253-
});
248+
);
254249

255250
const analyticsInstance: FirebaseAnalyticsInternal = {
256251
app,

packages/analytics/src/functions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ async function handleInitializationPromiseErrors(
3232
try {
3333
return await initializationPromise;
3434
} catch (e) {
35-
if (e.message.includes(AnalyticsError.INVALID_INDEXED_DB_CONTEXT)) {
35+
if (e.message.includes(AnalyticsError.INVALID_ANALYTICS_CONTEXT)) {
3636
logger.warn(e.message);
3737
return null;
3838
}

packages/analytics/src/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ async function gtagOnConfig(
9898
} catch (e) {
9999
// Thrown by an initializeIds promise in initializationPromisesMap before
100100
// making any fetches if indexedDB.open() is unavailable in the environment.
101-
if (e.message.includes(AnalyticsError.INVALID_INDEXED_DB_CONTEXT)) {
101+
if (e.message.includes(AnalyticsError.INVALID_ANALYTICS_CONTEXT)) {
102102
logger.warn(e.message);
103103
return;
104104
}
@@ -175,7 +175,7 @@ async function gtagOnEvent(
175175
} catch (e) {
176176
// Thrown by an initializeIds promise in initializationPromisesMap before
177177
// making any fetches if indexedDB.open() is unavailable in the environment.
178-
if (e.message.includes(AnalyticsError.INVALID_INDEXED_DB_CONTEXT)) {
178+
if (e.message.includes(AnalyticsError.INVALID_ANALYTICS_CONTEXT)) {
179179
logger.warn(e.message);
180180
return;
181181
}

0 commit comments

Comments
 (0)