firebase / firebase-tools

The Firebase Command Line Tools
MIT License
4.01k stars 925 forks source link

firebase-tools emulators fails silently when firebase client code is initialized with a different project config #2874

Open cbdeveloper opened 3 years ago

cbdeveloper commented 3 years ago

[REQUIRED] Environment info

firebase-tools: @8.16.2

Platform: Windows 10

[REQUIRED] Test case

I've described the best I could. I hope it helps to improve the package.

[REQUIRED] Steps to reproduce

I was getting strange behaviors from firebase Firestore emulator. It seems that the problem was that firebase-tools had a different selected project as from the project that was being initialized by my client code with: firebase.initializeApp(config);.

What I mean is:

firebase.initializeApp(config) was pointing to a .env file that had the config for a project-old keys.

.env

FIREBASE_APP_API_KEY="OLD_KEY"
FIREBASE_APP_AUTH_DOMAIN="project-old.firebaseapp.com"
FIREBASE_APP_DATABASE_URL="https://project-old.firebaseio.com"
FIREBASE_APP_PROJECT_ID="project-old"
FIREBASE_APP_STORAGE_BUCKET="project-old.appspot.com"
FIREBASE_APP_MESSAGING_SENDER_ID="OLD_ID"
FIREBASE_APP_ID="OLD_ID"

And firebase-tools was set to: project-new. So, when I ran firebase projects:list, this is what I was getting:

image

[REQUIRED] Expected behavior

I was expecting to get some warning about that difference between projects.

[REQUIRED] Actual behavior

My client code was able to read data from Firestore emulator. But only the initial data. The one that was initialized with the emulator. It was unable to read any new data from the Firestore. For example: I would go to the emulators UI on localhost:4000 and update some data. Even after refreshing my app, the app would still read the initialData and not the updated data. Also, no real time onSnapshot listeners were activated.

Ideally, I think that firebase-tools should be aware that its emulator is being called by a different firebase app, that was initialized on another project, and would show some kind of warning or error.

I feel that the emulator was failing silently and it was hard to find out what was wrong. No warnings or errors were being shown.

When I updated my client code to initializeApp(config) to the project-new keys, everything started to work fine again.

This is how I'm initializing the emulators:

firebase emulators:start --import=./src/firebase/emulatorData

This will initialize the emulators with the data from ./src/firebase/emulatorData

And on my client code I have:

firebase.ts

firebase.initializeApp(config);

if (DEVELOPMENT) {
  console.log(">>> From firebase.ts: activating emulators...");
  firebase.functions().useFunctionsEmulator("http://localhost:5001");
  firebase.firestore().settings({
    host: "localhost:8080",
    ssl: false
  });
}
google-oss-bot commented 3 years ago

This issue does not have all the information required by the template. Looks like you forgot to fill out some sections. Please update the issue with more information.

samtstern commented 3 years ago

@cbdeveloper thanks for the feedback. So just to make sure I understand this correctly, the change you'd like to see is for the Firebase CLI to print a warning when there is access to Firestore using a different project ID than the default?

itsravenous commented 3 years ago

I have just lost an hour or so to this :sweat_smile: Definitely user error, but a warning would be really, really useful! I think if I hadn't found this issue it would have taken me much longer to realise on my own what was happening. In my case it was quite easy to fall into because I have a few separate firebase projects for my app (dev/staging/prod) - I had my emulator using the dev project ID but my local app was pointing at staging (for reasons best known to the version of me from last week...)

cbdeveloper commented 3 years ago

Hey @samtstern , the emulator is run by firebase-tools, correct? From my experience with this issue, it seems that firebase-tools only responds to calls made from firebase/apps that have been initialized to the same project as the currently active one on firebase-tools. I don't think it's necessarily the default project on .firebaserc, if that's what you meant. But we would have to test it to confirm it.

firebase-tools probably already knows somehow that the call to the emulator is being made from a specific initialized project X from client code. If that project X is different than the one that is being used on the firestore emulator (which is probably the one that is active when you list firebase projects:list), it should display some kind of warning/error message, given the fact that in this situation (from my experience) it will only serve the initially loaded data from the emulator and the client code won't see any updates made to the data.

@itsravenous experience seems to confirm the issue described here. Like me, he also has multiple firebase projects configured for multiple environments.

samtstern commented 3 years ago

@cbdeveloper yes that's correct, the emulator suite emulates the currently active project as determined by firebase-tools. That's the project which will be shown in the Emulator UI and that's also the project which will be used when configuring Cloud Functions triggers locally.

The emulators are actually run as a "swarm" of local processes and they accept requests directly, requests are not proxied through firebase-tools. So to implement this type of warning we'd have to make a change to each individual emulator (Firestore, Realtime Database, etc). Not impossible but definitely a lot of work! We'd also have to find some way to disable this warning for unit testing because many developers use randomly generated project IDs to create isolated unit tests.

cbdeveloper commented 3 years ago

@samtstern thanks for your reply. If it's not feasible to implement it, I guess a warning about this issue should be added to the documentation. Maybe on this section:

https://firebase.google.com/docs/emulator-suite/install_and_configure

charles-allen commented 3 years ago

Given that:

Why can't we completely ignore the project ID in emulator mode?

* this is an assumption based on my experience that the Emulator UI & functions triggers don't seem to work if the projectIds don't line up, but I might be wrong

samtstern commented 3 years ago

@charles-allen what do you mean by "completely ignore the project ID"?

charles-allen commented 3 years ago

@charles-allen what do you mean by "completely ignore the project ID"?

For me, I only connect to one project, and I want everything connected to that project: emulated functions, emulated firestore, emulated auth, emulator UI, admin sdk, client sdk.

I'm already required to explicitly initialize the SDKs in emulator mode:

// For the Admin SDK:
if (useEmulator) {
  ...
  process.env.FIREBASE_AUTH_EMULATOR_HOST = `localhost:${firebaseJson.emulators.auth.port}`
  process.env.FIRESTORE_EMULATOR_HOST = `localhost:${firebaseJson.emulators.firestore.port}`
}

- OR -

// For the Client SDK:
if (useEmulator) {
  auth.useEmulator(`http://localhost:${EMU_PORT_AUTH}/`)
  db.useEmulator('localhost', parseInt(EMU_PORT_FIRESTORE))
  functions.useEmulator('localhost', parseInt(EMU_PORT_FUNCTIONS))
}

And by default the emulator automatically takes the active project from firebase use; so from my perspective, my calls could just be directed to that project, irrespective of my config, given that I've already made it clear that I want to use the emulator.

I suspect that this logic fails because my first assumption is wrong, that the emulator can actually support multiple projects, and that (unlike me) some people connect to multiple databases without caring that the Emulator UI (and possibly functions) are not available.

samtstern commented 3 years ago

@charles-allen I agree this is super confusing and there's a fundamental mismatch at the center which makes it tricky to solve.

(1) The individual service emulators (Firestore, RTDB, Functions, etc) which make up the Emulator Suite run as their own processes and emulate the production services. In most cases the Project ID is part of the request and just like the production service these emulators are happy to process requests for different projects! So if you write to the Firestore emulator with 100 different project IDs it will just keep track of 100 different databases.

(2) The "emulator suite" is what we call the controller in the CLI that helps manage this swarm of emulators. So it's the one that does things like register functions triggers. Unlike the individual services it has a strong notion of a project. So when it wires up Cloud Functions triggers, for instance, it does them under a single project ID.

(3) Despite the fact that the CLI is running the show, the SDKs still communicate directly with each service emulator. So there's no chance for us to intercept the request and say "that doesn't look like the right project ID".

And you might say "well (1) is really dumb behavior!" and in most cases you'd be right but when people write unit tests for the RTDB/Firestore emulators it's a very common pattern to use randomized project IDs to segregate data between test runs. In hindsight this is probably something we shouldn't have allowed but we can't break it now.

So hopefully that's a useful explanation! I do still hope we can do something to alleviate this confusion (besides just make the docs better) but we have to find a way that's not a breaking change.

charles-allen commented 3 years ago

It definitely makes more sense once you know a bit about how the emulator works :) Thanks for your help!

Some feedback based on my experience:

samtstern commented 3 years ago

@charles-allen thank you for the detailed feedback! Those are all good points and I will share them with the team. There's one I want to comment on:

I think it's non-intuitive for the emulator to default to a production project ID

There are two main reasons why we chose to go this route rather than using some fake project id "foo-bar-baz-123":

  1. Access to non-emulated services - right now we only emulate 4 of the ~20 services Firebase offers. When your app tries to access a non-emulated service we need to make sure we access the right project! For that reason we need a real project ID. An example use case is accessing Storage (which has no emulator) from the Cloud Functions emulator.
  2. Minimal Code Changes - we want the app you test in the emulators to be as close as possible to the app you deploy. The only change should be a small block of useEmulator() calls (which we can't really avoid). By using a production project ID you can keep your initializeApp() block unchanged.