fix: use the modular FieldValue so the functions work in the emulator - #2989
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2989 +/- ##
==========================================
- Coverage 86.38% 86.38% -0.01%
==========================================
Files 996 996
Lines 56858 56858
Branches 15060 15060
==========================================
- Hits 49117 49115 -2
- Misses 7721 7723 +2
Partials 20 20
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The functions emulator replaces the firebase-admin module with a proxy. Reading admin.firestore returns Proxied.getOriginal(target, "firestore"), which is value.bind(target). A bound function is a new function object and does not carry the original's own properties, so admin.firestore.FieldValue is undefined in the emulator. It works normally in a plain node process and in deployed functions, which is why this went unnoticed. Any function that reached a serverTimestamp() call therefore failed with "TypeError: Cannot read properties of undefined (reading 'serverTimestamp')". The emulator stopped the function, nothing was recorded, and for the queue functions the entry was left in place. That made the AI analysis pipeline impossible to run end to end against the emulators. Importing FieldValue from firebase-admin/firestore avoids the namespace and behaves the same in both places. This changes all six call sites, in on-analysis-document-pending, on-analysis-document-imaged, post-document-comment, post-exemplar-comment and on-class-data-doc-written. admin.firestore.FieldValue was the only namespace static used anywhere in functions-v2/src, so nothing of this kind is left. on-class-data-doc-written no longer imports firebase-admin, since that was its only use of it. Each changed file carries a one-line note saying why FieldValue is imported separately, so the two imports of firebase-admin do not look like a mistake. The full explanation is here rather than repeated in the code. No test covers this. The tests in functions-v2 call the functions directly, so firebase-admin is never proxied and they cannot tell the difference. A check that read the source for the old form was considered and dropped: it could not distinguish a runtime call from a type annotation such as admin.firestore.Firestore, which is erased at compile time and safe, so it would have failed on correct code. Verified by hand against the emulator. With the change, the whole AI analysis pipeline ran end to end through the CLUE user interface: a text-only document, a drawing-only document, an empty document, and the mock evaluator, each producing a comment. Posting a comment from the comments panel exercised post-document-comment, and calling the postExemplarComment endpoint directly exercised post-exemplar-comment. Both wrote a real createdAt timestamp. on-class-data-doc-written was not run, because it makes a paid OpenAI call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
adecdf2 to
541ad0f
Compare
collaborative-learning
|
||||||||||||||||||||||||||||
| Project |
collaborative-learning
|
| Branch Review |
fix-fieldvalue-under-functions-emulator
|
| Run status |
|
| Run duration | 10m 47s |
| Commit |
|
| Committer | Ethan McElroy |
| View all properties for this run ↗︎ | |
| Test results | |
|---|---|
|
|
0
|
|
|
1
|
|
|
5
|
|
|
0
|
|
|
221
|
| View all changes introduced in this branch ↗︎ | |
There was a problem hiding this comment.
Pull request overview
This pull request fixes Firebase Functions emulator failures caused by admin.firestore being proxied/bound such that namespace statics (notably admin.firestore.FieldValue) become undefined. It updates affected functions to use the modular FieldValue import from firebase-admin/firestore, so serverTimestamp() works consistently in both emulator and deployed environments.
Changes:
- Replaced
admin.firestore.FieldValue.serverTimestamp()withFieldValue.serverTimestamp()at all identified call sites. - Added modular
FieldValueimports (and removed the unusedfirebase-adminimport where it was only needed forFieldValue). - Added inline comments explaining the emulator/proxy behavior motivating the modular import.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| functions-v2/src/post-exemplar-comment.ts | Uses modular FieldValue for createdAt timestamp when posting exemplar comments. |
| functions-v2/src/post-document-comment.ts | Uses modular FieldValue for createdAt timestamp when posting document comments. |
| functions-v2/src/on-class-data-doc-written.ts | Removes firebase-admin import and uses modular FieldValue for summaryCreatedAt. |
| functions-v2/src/on-analysis-document-pending.ts | Uses modular FieldValue for docImaged timestamp when advancing queue docs. |
| functions-v2/src/on-analysis-document-imaged.ts | Uses modular FieldValue for createdAt and completedAt timestamps when writing analysis results. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The problem
Running the AI analysis pipeline against the Firebase emulators fails as soon as a function reaches a
serverTimestamp()call:The emulator stops the function, nothing is recorded, and for the queue functions the entry is left in place. Those triggers have no retry configured, so the document is never processed again.
Why it happens
The functions emulator replaces the
firebase-adminmodule with a proxy. When code readsadmin.firestore, the proxy returnsProxied.getOriginal(target, "firestore"), which is:A bound function is a new function object, and it does not carry the original's own properties. So the statics on
admin.firestore—FieldValueand the rest — are undefined inside the emulator. Checked directly:It works normally in a plain node process and in deployed functions, which is why this went unnoticed until someone tried to run the pipeline locally.
The change
Import
FieldValuefromfirebase-admin/firestoreinstead of reaching for it through theadmin.firestorenamespace. That avoids the proxy and behaves the same in both places.src/chat/drain.tsalready used the modular imports and explains why. This brings the rest of the directory in line with it.All six call sites, in five files:
on-analysis-document-pending.tsdocImagedon-analysis-document-imaged.tscreatedAton-analysis-document-imaged.tsdonerecord'scompletedAtpost-document-comment.tscreatedAtpost-exemplar-comment.tscreatedAton-class-data-doc-written.tssummaryCreatedAtadmin.firestore.FieldValuewas the only namespace static used anywhere infunctions-v2/src— noTimestamp, noFieldPath, noadmin.database.ServerValue— so this covers all of it rather than part of it.on-class-data-doc-written.tsno longer importsfirebase-adminat all, since that was its only use of it.Testing
The existing suite passes, unchanged at 122 tests, along with typecheck and lint.
No test covers this change, and that is deliberate. The tests in
functions-v2call the functions directly rather than through the emulator's runtime, sofirebase-adminis never proxied and they cannot tell the two forms apart. They confirm the change breaks nothing; they cannot confirm it fixes anything.A check that read the source files and failed on the old form was written and then dropped. It could not tell a runtime call from a type annotation such as
admin.firestore.Firestore, which TypeScript erases at compile time and which is perfectly safe.What was verified by hand
All against the emulators, with the change applied.
The full AI analysis pipeline ran end to end through the CLUE user interface — a text-only document, a drawing-only document, an empty document, and the mock evaluator — each producing a comment in the panel. Before the change, every one of those runs stopped at the first
serverTimestamp()call.Posting a comment from the comments panel exercised
post-document-comment. Calling thepostExemplarCommentendpoint directly exercisedpost-exemplar-comment. Both wrote a realcreatedAttimestamp.on-class-data-doc-writtenwas not run, because triggering it makes a paid OpenAI call. Its change is identical to the other five.