Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 41 additions & 36 deletions src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ declare var window: any;

export class FirebaseAnalyticsWeb
extends WebPlugin
implements FirebaseAnalyticsPlugin {
implements FirebaseAnalyticsPlugin
{
private not_supported_mssg = "This method is not supported";
private options_missing_mssg = "Firebase options are missing";
private duplicate_app_mssg = "Firebase app already exists";
Expand All @@ -15,6 +16,7 @@ export class FirebaseAnalyticsWeb

public readonly ready: Promise<any>;
private readyResolver: Function;
private readyReject: Function;
private analyticsRef: any;

private scripts = [
Expand All @@ -30,7 +32,11 @@ export class FirebaseAnalyticsWeb

constructor() {
super();
this.ready = new Promise((resolve) => (this.readyResolver = resolve));
this.ready = new Promise(
(resolve, reject) => (
(this.readyResolver = resolve), (this.readyReject = reject)
),
);
this.configure();
}

Expand All @@ -40,10 +46,9 @@ export class FirebaseAnalyticsWeb
* @returns firebase analytics object reference
* Platform: Web
*/
initializeFirebase(options: FirebaseInitOptions): Promise<any> {
return new Promise(async (resolve, reject) => {
await this.ready;

async initializeFirebase(options: FirebaseInitOptions): Promise<any> {
await this.ready;
return new Promise((resolve, reject) => {
if (this.hasFirebaseInitialized()) {
reject(this.duplicate_app_mssg);
return;
Expand All @@ -65,10 +70,9 @@ export class FirebaseAnalyticsWeb
* @param options - userId: unique identifier of the user to log
* Platform: Web/Android/iOS
*/
setUserId(options: { userId: string }): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async setUserId(options: { userId: string }): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand All @@ -92,10 +96,12 @@ export class FirebaseAnalyticsWeb
* value: The value of the user property.
* Platform: Web/Android/iOS
*/
setUserProperty(options: { name: string; value: string }): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async setUserProperty(options: {
name: string;
value: string;
}): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand Down Expand Up @@ -156,10 +162,9 @@ export class FirebaseAnalyticsWeb
* params: the map of event parameters.
* Platform: Web/Android/iOS
*/
logEvent(options: { name: string; params: object }): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async logEvent(options: { name: string; params: object }): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand All @@ -185,10 +190,9 @@ export class FirebaseAnalyticsWeb
* @param options - enabled: boolean true/false to enable/disable logging
* Platform: Web/Android/iOS
*/
setCollectionEnabled(options: { enabled: boolean }): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async setCollectionEnabled(options: { enabled: boolean }): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand Down Expand Up @@ -218,10 +222,9 @@ export class FirebaseAnalyticsWeb
return this.analyticsRef;
}

enable(): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async enable(): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand All @@ -232,10 +235,9 @@ export class FirebaseAnalyticsWeb
});
}

disable(): Promise<void> {
return new Promise(async (resolve, reject) => {
await this.ready;

async disable(): Promise<void> {
await this.ready;
return new Promise((resolve, reject) => {
if (!this.analyticsRef) {
reject(this.analytics_missing_mssg);
return;
Expand All @@ -261,7 +263,8 @@ export class FirebaseAnalyticsWeb
this.analyticsRef = window.firebase.analytics();
}
} catch (error) {
throw error;
this.readyReject(error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this change?

Copy link
Author

@TomVHPresto TomVHPresto Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error being thrown here means that the ready promise is never rejected, so if you do await FirebaseAnalytics.initialize() you can't catch the error using a try / catch, and the call awaits forever. This was causing an issue for me because I work on a web app that can be used offline, so I need to be able to handle his gracefully.

return;
}

const interval = setInterval(() => {
Expand Down Expand Up @@ -289,12 +292,14 @@ export class FirebaseAnalyticsWeb
return resolve(null);
}

await this.loadScript(firebaseAppScript.key, firebaseAppScript.src);
await this.loadScript(
firebaseAnalyticsScript.key,
firebaseAnalyticsScript.src
resolve(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this change?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar reasoning here, using await will throw an exception on error which causes an unhandled promise rejection - instead resolving it using this promise propagates any error by rejecting the promise.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I was wrong about this actually and only the other change is necessary. Let me do some testing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems it won't work without the change in my testing. Using async / await inside a new promise constructor is an anti-pattern because if it throws inside an async callback, the error isn't propagated correctly.

this.loadScript(firebaseAppScript.key, firebaseAppScript.src).then(() =>
this.loadScript(
firebaseAnalyticsScript.key,
firebaseAnalyticsScript.src,
),
),
);
resolve(null);
});
}

Expand Down