Skip to content

Commit 3035605

Browse files
authored
Merge 64df7e5 into c2b737b
2 parents c2b737b + 64df7e5 commit 3035605

File tree

8 files changed

+138
-7
lines changed

8 files changed

+138
-7
lines changed

.changeset/clean-numbers-flow.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@firebase/analytics": minor
3+
"firebase": minor
4+
---
5+
6+
Issue 2393 fix - analytics module
7+
8+
- Added a public method `isSupported` to Analytics module which returns true if current browser context supports initialization of analytics module.
9+
- Added runtime checks to Analytics module that validate if cookie is enabled in current browser and if current browser environment supports indexedDB functionalities.

packages/analytics/index.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ import {
3232
ComponentContainer
3333
} from '@firebase/component';
3434
import { ERROR_FACTORY, AnalyticsError } from './src/errors';
35-
35+
import {
36+
isIndexedDBAvailable,
37+
validateIndexedDBOpenable,
38+
areCookiesEnabled
39+
} from '@firebase/util';
3640
import { name, version } from './package.json';
3741

3842
declare global {
@@ -45,6 +49,7 @@ declare global {
4549
* Type constant for Firebase Analytics.
4650
*/
4751
const ANALYTICS_TYPE = 'analytics';
52+
4853
export function registerAnalytics(instance: _FirebaseNamespace): void {
4954
instance.INTERNAL.registerComponent(
5055
new Component(
@@ -55,13 +60,13 @@ export function registerAnalytics(instance: _FirebaseNamespace): void {
5560
const installations = container
5661
.getProvider('installations')
5762
.getImmediate();
58-
5963
return factory(app, installations);
6064
},
6165
ComponentType.PUBLIC
6266
).setServiceProps({
6367
settings,
64-
EventName
68+
EventName,
69+
isSupported
6570
})
6671
);
6772

@@ -97,8 +102,31 @@ registerAnalytics(firebase as _FirebaseNamespace);
97102
declare module '@firebase/app-types' {
98103
interface FirebaseNamespace {
99104
analytics(app?: FirebaseApp): FirebaseAnalytics;
105+
isSupported(): Promise<boolean>;
100106
}
101107
interface FirebaseApp {
102108
analytics(): FirebaseAnalytics;
103109
}
104110
}
111+
112+
/**
113+
* this is a public static method provided to users that wraps three different checks:
114+
*
115+
* 1. check if cookie is enabled in current browser.
116+
* 2. check if IndexedDB is supported by the browser environment.
117+
* 3. check if the current browser context is valid for using IndexedDB.
118+
*/
119+
async function isSupported(): Promise<boolean> {
120+
if (!areCookiesEnabled()) {
121+
return false;
122+
}
123+
if (!isIndexedDBAvailable()) {
124+
return false;
125+
}
126+
try {
127+
const isDBOpenable: boolean = await validateIndexedDBOpenable();
128+
return isDBOpenable;
129+
} catch (error) {
130+
return false;
131+
}
132+
}

packages/analytics/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@
5656
],
5757
"reportDir": "./coverage/node"
5858
}
59-
}
59+
}

packages/analytics/src/errors.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export const enum AnalyticsError {
2222
NO_GA_ID = 'no-ga-id',
2323
ALREADY_EXISTS = 'already-exists',
2424
ALREADY_INITIALIZED = 'already-initialized',
25-
INTEROP_COMPONENT_REG_FAILED = 'interop-component-reg-failed'
25+
INTEROP_COMPONENT_REG_FAILED = 'interop-component-reg-failed',
26+
INDEXED_DB_UNSUPPORTED = 'indexedDB-unsupported',
27+
INVALID_INDEXED_DB_CONTEXT = 'invalid-indexedDB-context',
28+
COOKIES_NOT_ENABLED = 'cookies-not-enabled'
2629
}
2730

2831
const ERRORS: ErrorMap<AnalyticsError> = {
@@ -39,12 +42,21 @@ const ERRORS: ErrorMap<AnalyticsError> = {
3942
'settings() must be called before initializing any Analytics instance' +
4043
'or it will have no effect.',
4144
[AnalyticsError.INTEROP_COMPONENT_REG_FAILED]:
42-
'Firebase Analytics Interop Component failed to instantiate'
45+
'Firebase Analytics Interop Component failed to instantiate',
46+
[AnalyticsError.INDEXED_DB_UNSUPPORTED]:
47+
'IndexedDB is not supported by current browswer',
48+
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]:
49+
"Environment doesn't support IndexedDB: {$errorInfo}. " +
50+
'Wrap initialization of analytics in analytics.isSupported() ' +
51+
'to prevent initialization in unsupported environments',
52+
[AnalyticsError.COOKIES_NOT_ENABLED]:
53+
'Cookies are not enabled in this browser environment. Analytics requires cookies to be enabled.'
4354
};
4455

4556
interface ErrorParams {
4657
[AnalyticsError.ALREADY_EXISTS]: { id: string };
4758
[AnalyticsError.INTEROP_COMPONENT_REG_FAILED]: { reason: Error };
59+
[AnalyticsError.INVALID_INDEXED_DB_CONTEXT]: { errorInfo: string };
4860
}
4961

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

packages/analytics/src/factory.ts

+18
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ import { ANALYTICS_ID_FIELD } from './constants';
3838
import { AnalyticsError, ERROR_FACTORY } from './errors';
3939
import { FirebaseApp } from '@firebase/app-types';
4040
import { FirebaseInstallations } from '@firebase/installations-types';
41+
import {
42+
isIndexedDBAvailable,
43+
validateIndexedDBOpenable,
44+
areCookiesEnabled
45+
} from '@firebase/util';
4146

4247
/**
4348
* Maps gaId to FID fetch promises.
@@ -117,6 +122,19 @@ export function factory(
117122
app: FirebaseApp,
118123
installations: FirebaseInstallations
119124
): FirebaseAnalytics {
125+
if (!areCookiesEnabled()) {
126+
throw ERROR_FACTORY.create(AnalyticsError.COOKIES_NOT_ENABLED);
127+
}
128+
if (!isIndexedDBAvailable()) {
129+
throw ERROR_FACTORY.create(AnalyticsError.INDEXED_DB_UNSUPPORTED);
130+
}
131+
// Async but non-blocking.
132+
validateIndexedDBOpenable().catch(error => {
133+
throw ERROR_FACTORY.create(AnalyticsError.INVALID_INDEXED_DB_CONTEXT, {
134+
errorInfo: error
135+
});
136+
});
137+
120138
const analyticsId = app.options[ANALYTICS_ID_FIELD];
121139
if (!analyticsId) {
122140
throw ERROR_FACTORY.create(AnalyticsError.NO_GA_ID);

packages/analytics/src/helpers.ts

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import {
3232
} from './constants';
3333
import { FirebaseInstallations } from '@firebase/installations-types';
3434
import { logger } from './logger';
35-
3635
/**
3736
* Initialize the analytics instance in gtag.js by calling config command with fid.
3837
*

packages/firebase/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -5083,6 +5083,16 @@ declare namespace firebase.analytics {
50835083
id?: string;
50845084
name?: string;
50855085
}
5086+
5087+
/**
5088+
* An async function that returns true if current browser context supports initialization of analytics module
5089+
* (`firebase.analytics()`).
5090+
*
5091+
* Returns false otherwise.
5092+
*
5093+
*
5094+
*/
5095+
function isSupported(): Promise<boolean>;
50865096
}
50875097

50885098
declare namespace firebase.auth.Auth {

packages/util/src/environment.ts

+55
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,58 @@ export function isSafari(): boolean {
134134
!navigator.userAgent.includes('Chrome')
135135
);
136136
}
137+
138+
/**
139+
* This method checks if indexedDB is supported by current browser
140+
* @return true if indexedDB is supported by current browser
141+
*/
142+
export function isIndexedDBAvailable(): boolean {
143+
if (!('indexedDB' in window) || indexedDB === null) {
144+
return false;
145+
}
146+
return true;
147+
}
148+
149+
/**
150+
* This method validates browser context for indexedDB by opening a dummy indexedDB database and reject
151+
* if errors occur during the database open operation.
152+
*/
153+
export function validateIndexedDBOpenable(): Promise<boolean> {
154+
return new Promise((resolve, reject) => {
155+
try {
156+
let preExist: boolean = true;
157+
const DB_CHECK_NAME =
158+
'validate-browser-context-for-indexeddb-analytics-module';
159+
const request = window.indexedDB.open(DB_CHECK_NAME);
160+
request.onsuccess = () => {
161+
request.result.close();
162+
// delete database only when it doesn't pre-exist
163+
if (!preExist) {
164+
window.indexedDB.deleteDatabase(DB_CHECK_NAME);
165+
}
166+
resolve(true);
167+
};
168+
request.onupgradeneeded = () => {
169+
preExist = false;
170+
};
171+
172+
request.onerror = () => {
173+
reject(request.error?.message || '');
174+
};
175+
} catch (error) {
176+
reject(error);
177+
}
178+
});
179+
}
180+
181+
/**
182+
*
183+
* This method checks whether cookie is enabled within current browser
184+
* @return true if cookie is enabled within current browser
185+
*/
186+
export function areCookiesEnabled(): boolean {
187+
if (!navigator || !navigator.cookieEnabled) {
188+
return false;
189+
}
190+
return true;
191+
}

0 commit comments

Comments
 (0)