Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User hasn't closed app for a couple of days results in INTERNAL ASSERTION FAILED: Unexpected state #8383

Open
TimmNL opened this issue Jul 22, 2024 · 12 comments

Comments

@TimmNL
Copy link

TimmNL commented Jul 22, 2024

Operating System

MacOS 14.5

Browser Version

Safari, Chrome, Firefox

Firebase SDK Version

10.12.4

Firebase SDK Product:

AppCheck, Auth, Firestore, Functions

Describe your project's tooling

React with Typescript, Tanstack Query, Vitest and Vite

Describe the problem

I'm trying to create a web-app with offline support, in which the logged in user can create, update and delete todo's. Currently I have implemented firebase auth, firestore, firebase appCheck and firebase functions to trigger some cloud functions when the user is online.

The problem I'm having is that users complain that when they have the web-app opened for a couple of days, they eventually get the following error:
INTERNAL ASSERTION FAILED: Unexpected state.
Scherm­afbeelding 2024-07-22 om 17 06 04

When the user reloads the page, all data is fetched again and everything works fine.

After a little research I found that sometimes the user isn't logged in anymore (firebase/auth's currentUser is null), until they reload the app. It doesn't matter if the user is online or offline, the reload logs the user back in.

To solve this I added some extra code to my auth-handler, which resulted in not being able to share the state between different tabs of the same browser. I'd like to have that functionality back and also a fix of some sort for users that have lost the connection to the app until they reload the app.

Steps and code to reproduce issue

In my app I use singeltons for the app, database and auth to make sure they stay alive, even when the user isn't actively using them. The calls are all done through tanstack query.

My initialization of the firestore app looks like this:

// init-app.ts
import { deleteApp, FirebaseApp, initializeApp } from 'firebase/app';

let app: FirebaseApp | undefined;

export const initApp = () => {
  if (!app) {
    app = initializeApp({
      apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
      authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
      projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
      storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
      messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
      appId: import.meta.env.VITE_FIREBASE_APP_ID,
      measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
    });
  }

  return app;
};

export const clearApp = async () => {
  if (app) {
    await deleteApp(app);
  }
  app = undefined;
};

initialization of firebase auth:

import {
  Auth,
  browserLocalPersistence,
  connectAuthEmulator,
  getAuth,
  onAuthStateChanged,
  setPersistence,
} from 'firebase/auth';

import { initAppCheck } from './app-check';
import { initApp } from './init-app';

let auth: Auth | undefined;

export const getAuthentication = async () => {
  if (!auth) {
    import.meta.env.MODE !== 'development' && initAppCheck(initApp());
    auth = getAuth(initApp());

    if (
      import.meta.env.MODE === 'development' &&
      import.meta.env.VITE_FIREBASE_EMULATOR_URL
    ) {
      connectAuthEmulator(
        auth,
        `http://${import.meta.env.VITE_FIREBASE_EMULATOR_URL}:9099`,
      );
    }

    await setPersistence(auth, browserLocalPersistence);

    // todo: this part is not realy preferred since it breaks local persistence over multiple tabs
    onAuthStateChanged(auth, async (user) => {
      if (import.meta.env.MODE === 'development') {
        // eslint-disable-next-line no-console -- only show on dev
        console.log('user state changed', user?.uid);
      }

      await user?.getIdToken(true);

      if (import.meta.env.MODE === 'development') {
        // eslint-disable-next-line no-console -- only show on dev
        console.log('user state changed - getIdToken called', user?.uid);
      }
    });
  }

  return auth;
};

export const clearAuth = () => {
  auth = undefined;
};

initializing firestore database:

import {
  connectFirestoreEmulator,
  disableNetwork,
  enableNetwork,
  Firestore,
  initializeFirestore,
  persistentLocalCache,
  persistentMultipleTabManager,
} from 'firebase/firestore';

import { getAnalytics } from './get-analytics';
import { getPerformanceMonitoring } from './get-performance-monitoring';
import { initApp } from './init-app';

let database: Firestore | undefined;

export const getDatabase = () => {
  if (!database) {
    database = initializeFirestore(initApp(), {
      localCache: persistentLocalCache(
        /*settings*/ { tabManager: persistentMultipleTabManager() },
      ),
    });

    if (
      import.meta.env.MODE === 'development' &&
      import.meta.env.VITE_FIREBASE_EMULATOR_URL
    ) {
      connectFirestoreEmulator(
        database,
        import.meta.env.VITE_FIREBASE_EMULATOR_URL,
        8080,
      );
    }

    // todo: this is added to try to help firestore notice that it should use the local or online database
    const handleOnline = () => {
      if (database) {
        enableNetwork(database);
      }
    };

    const handleOffline = () => {
      if (database) {
        disableNetwork(database);
      }
    };

    const handleVisibilityChange = () => {
      if (database) {
        document.visibilityState === 'visible'
          ? enableNetwork(database)
          : disableNetwork(database);
      }
    };

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    window.addEventListener('visibilitychange', handleVisibilityChange);

    getAnalytics(initApp());
    getPerformanceMonitoring(initApp());
  }

  return database;
};

export const clearDatabase = () => {
  database = undefined;
};

Creating a query

// get-task-by-id.ts
import { doc } from 'firebase/firestore';
import { ID } from 'shared/types/id';

import { taskConverter } from './converters/task';
import { CollectionOptions } from './helpers';
import { getDatabase } from './helpers/get-database';

export const getTaskById = async (id: ID) => {
  return doc(getDatabase(), CollectionOptions.Tasks, id).withConverter(
    taskConverter,
  );
};

Tanstack query:

// use-task-by-id.ts
import { useQuery } from '@tanstack/react-query';
import { createSubscriptionDoc } from 'shared/lib/@tanstack-query';
import { getTaskById } from './get-task-by-id';
import { ID } from 'shared/types/id';

export const queryKey = 'task-by-id';

export const useTaskByIdQuery = (uid: ID = '', id: ID = '') =>
  useQuery({
    queryKey: [queryKey, uid, id],
    queryFn: createSubscriptionDoc(() => getTaskById(id)),
    enabled: !!uid && !!id,
  });

Place where the error comes shows up:

import { QueryFunction } from '@tanstack/react-query';
import {
  DocumentReference,
  DocumentSnapshot,
  FirestoreError,
  getDocFromCache,
  onSnapshot,
} from 'firebase/firestore';
import { ID } from 'shared/types/id';

import { queryClient } from './query-client';
import { addSubscription } from './subscriptions';

export const createSubscriptionDoc = <T extends { id: ID }>(
  doc: () => Promise<DocumentReference<T>> | DocumentReference<T>,
): QueryFunction<T | undefined, string[]> => {
  let firstRun = true;
  let latestSnapshotData: T;

  return (context) => {
    return new Promise(async (resolve, reject) => {
      const docRef = await doc();

      const onSuccess = (response: DocumentSnapshot<T>) => {
        latestSnapshotData = response.data() as T;

        let data = latestSnapshotData;

        if (firstRun) {
          resolve(data);
          firstRun = false;
          return;
        }

        queryClient.setQueryData(context.queryKey, data);
      };

      const onError = (error: FirestoreError) => { // <-- this is executed and is where the error comes from
        if (firstRun) {
          reject(error);
          firstRun = false;
          return;
        }

        queryClient.invalidateQueries({ queryKey: context.queryKey });
      };

      getDocFromCache(docRef)
        .then(onSuccess)
        .catch(() => {
          // this is just cache, do nothing with this error
        });

      addSubscription(onSnapshot(docRef, onSuccess, onError));
    });
  };
};

As seen in the code above, I make use of the onSnapshot function to get all the updates.

I wonder if I'm the only one with this issue, I could not find an issue related to mine.

Thanks for the help!

@TimmNL TimmNL added new A new issue that hasn't be categoirzed as question, bug or feature request question labels Jul 22, 2024
@google-oss-bot
Copy link
Contributor

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

@TimmNL TimmNL changed the title User hasn't closed app for a couple of days results in INTERNAL ASSERTION FAILED: Unexpected state User hasn't closed app for a couple of days results in INTERNAL ASSERTION FAILED: Unexpected state Jul 22, 2024
@jbalidiong jbalidiong added api: firestore needs-attention and removed needs-triage new A new issue that hasn't be categoirzed as question, bug or feature request labels Jul 22, 2024
@TimmNL
Copy link
Author

TimmNL commented Jul 26, 2024

@jbalidiong Do you have a status update on this? Do you maybe know if someone is having a look into this?

@cherylEnkidu
Copy link
Contributor

Hi @TimmNL ,

Just want to double check, if removing force token refresh, the multi tab is working?

Could you please provide a log so that we can see what happened before internal assertion failed?

It would also be helpful if you can have a constant repo way since it is hard to debug if the only way triggering it is waiting for a couple of days.

The team thinks what is going on here might relate to two issues.

  1. The firebase app broken due to the uncaught exception throw from AsyncQueue, that's why checking what is the last calling function is important.

  2. Firestore has build in AuthToken refresh so refreshing on App developer site is unnecessary.

Could you please share more of "breaks local persistence over multiple tabs"?

It is referring to, for example you have one tab open and it listen from cache, the another tab is modifying the cache but the first tab didn't get update?

@TimmNL
Copy link
Author

TimmNL commented Jul 30, 2024

Hi @cherylEnkidu,

Thanks for the response!

You are correct; removing the following code snippet did indeed make it work in multiple tabs:

onAuthStateChanged(auth, async (user) => {
      if (import.meta.env.MODE === 'development') {
        // eslint-disable-next-line no-console -- only show on dev
        console.log('user state changed', user?.uid);
      }

      await user?.getIdToken(true);

      if (import.meta.env.MODE === 'development') {
        // eslint-disable-next-line no-console -- only show on dev
        console.log('user state changed - getIdToken called', user?.uid);
      }
    });

By "breaks local persistence over multiple tabs," I mean that after opening a second tab with the same site, the first tab starts throwing the same error as shown above. The newly opened tab, however, works fine and doesn't show any problems.

I've executed the following steps, this time running with my emulator:

Steps

  1. open the website in a first tab:
Scherm­afbeelding 2024-07-30 om 11 52 22
  1. Wait for 5-10 minutes (if earlier, it doesn't throw the error)

  2. Open up the website in a second tab while keeping the first tab opened.

  3. Check the console of the first tab again:

Scherm­afbeelding 2024-07-30 om 11 57 09

When I check the emulator logs, it states that some requests have come in with request.auth having the value null.

The reason for adding this was to ensure the user existed when executing a query, as occasionally, after having the app open for a couple of days, it would show that auth.currentUser.uid was null. My idea was that requesting a new token with user?.getIdToken(true) when the auth state changed would fetch a new token for the user, resolving the issue. However, the problem persists, and users still do not receive any data after a few days.

@TimmNL
Copy link
Author

TimmNL commented Aug 7, 2024

@cherylEnkidu or @jbalidiong, Do you have an update on this? I'm still stuck on it and I'm getting more complains from users about it. Thanks!

@TimmNL
Copy link
Author

TimmNL commented Aug 9, 2024

@cherylEnkidu and @jbalidiong , Today I got a different error message while working on a hosted version of the project. I got the following, in which the user is authorized but I get Target ID already exists: 4.
I'm not sure what this means, but I added the screenshot of it.
Hopefully this helps is some way
Scherm­afbeelding 2024-08-09 om 14 44 49

@cherylEnkidu
Copy link
Contributor

Hi @TimmNL ,

The team receives some bug report ticket related to exception throw from AsyncQueue. We are working on investigation to see whether they are related to the same root cause. In the meantime while I am working on reproducing this issue, could you please provide the stack trace of the crash?

@TimmNL
Copy link
Author

TimmNL commented Aug 20, 2024

@cherylEnkidu,

Here is a stack-trace, if I get a different outcome in another place, I'll add that trace as wel:
Scherm­afbeelding 2024-08-20 om 14 52 45

@TimmNL
Copy link
Author

TimmNL commented Aug 27, 2024

@cherylEnkidu Is the strack-trace I sent of any help? Do you have any update on what could be going on here and how I could fix it? Or might you have a work-around for the time being?
Thanks!

@TimmNL
Copy link
Author

TimmNL commented Aug 28, 2024

@cherylEnkidu,

I recently set up Rollbar to get better visibility on the bugs in my project. I just got this stack trace:
Scherm­afbeelding 2024-08-28 om 10 05 44
Hopefully, it helps in tracking down the issue!

@TimmNL
Copy link
Author

TimmNL commented Sep 9, 2024

@cherylEnkidu or @jbalidiong, Do you have any updates on the issue and do the stack-traces provide any information?

@cherylEnkidu
Copy link
Contributor

Hi @TimmNL ,

Thank you for providing the stack trace. The problem you are running into could be related to some under investigation crashes inside AsyncQueue. Here is a relevant ticket and its content might be able to inspire you: #8250

While investigating, could you please provide a full debug log just like the developer provided inside the other ticket?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants