Firebase functions just dying out with no explanation or error - firebase

I'm trying to implement a messaging application using Firebase Firestore and Firebase Cloud Functions.
In essence, chat messages are stored as individual documents in a subcollection. At first, I implemented this as directly adding a document from the client and listening on the collection and updating the clients when a change happens but later I decided to switch to using Cloud functions so that I can add some functionality that's better done on the server side(filtering etc.).
So I created a function for sending messages, which creates the documents on behalf of the users when the users call the function from the app(i.e. tap the send button).
The function worked and I was able to monitor the processes through the logs. Unfortunately, the functions began to die out without error, the console was reporting that the functions are executed successfully and it usually took less than a second to execute.
I suspect that it has something to do with the promises that probably continue to run but this is the same code that was working but failing today.
If I try a few more times, the functions seem to be working again. Do I need to keep the functions "warm"? Are cloud functions not reliable enough to handle this kind of tasks? When I say my user that a message is sent, I need to be able to confirm that it is sent and communicate it with the users if it failed.
It's hard to debug the issue because no errors are thrown(not even info message, it's just as if didn't happen), it just says that the function successfully finished execution and nothing happened.
Am I missing something here? Thank you.
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent")
}).catch(err => {
console.log("Error sending mesage:", err)
})
})

As explained in the documentation of the HTTP Callable Cloud Functions:
To return data after an asynchronous operation, return a promise.
Then follows an example:
const sanitizedMessage = sanitizer.sanitizeText(text); // Sanitize the message.
return admin.database().ref('/messages').push({
text: sanitizedMessage,
author: { uid, name, picture, email },
}).then(() => {
console.log('New Message written');
// Returning the sanitized message to the client.
return { text: sanitizedMessage };
})
So you need to adapt your code as follows:
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
//Here send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent");
return { text: "Message sent" };
}).catch(err => {
console.log("Error sending mesage:", err);
//Here, again, send back an error as explained here: https://firebase.google.com/docs/functions/callable#handle_errors
})
})
If you don't want to return a value to the client, you could do as follows, returning null when the Promise returned by the add() asynchronous method resolves. (Not tested but it should work).
exports.sendMessage = functions.https.onCall((data, context) => {
if (context.auth.uid == undefined) {
console.warn("SEND MESSAGE: USER NOT SIGNED IN");
return null;
}
console.log("Sending message:", data)
const matchId = data["matchId"];
const message = data["message"]
const uid = context.auth.uid
//Note the return on next line
return admin.firestore().collection(MatchingUsers).doc(matchId).collection(UserMessages).add({
type: "text",
from: uid,
message: message,
timestamp: admin.firestore.Timestamp.now()
}).then(result => {
console.log("Message sent"); //Actually, if you don't need this console.log() you can remove this entire then() block, returning the promise from add() is enough
return null;
}).catch(err => {
console.log("Error sending mesage:", err);
return null;
})
})

Related

Push Notifications for follow system fails after fetching FCM tokens from firestore

I'm writing a cloud function for my iOS app to watch for any follower changes in my users to notify them when someone follows them. My follower subcollection is within each user data document and I use wildcards to listen to any changes. I've also provided good logging during each step, so it's easy to see where the problem is, however, since I'm rather new to cloud functions, I don't know exactly how I'd fix it.
The cloud function is as follows.
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
let title
let body
let payload
let FCMRegistrationToken_KEY
exports.sendNotificationOnFollowerCreate = functions.firestore
.document('Users/{userID}/Followers/{followerID}')
.onCreate((snapshot, context) => {
if(snapshot.after){
// Get the userId and followerId
const userID = context.params.userID;
const followerID = context.params.followerID;
// Get the data of the follower document
const newData = snapshot.after.data()
const fullName = newData.firstName + " " + newData.lastName
title = 'Someone just followed you'
body = fullName + ' Just followed you right now!\n' + 'username: ' + newData.userName
// Create the notification payload
payload = {
notification: {
title: title,
body: body
}
}
// Get FMC token by fetching the FCMToken Document for the userID above.
admin.firestore().collection('FCMTokens').doc(userID).get().then(doc => {
if(!doc.exists) {
console.log('User not found!');
} else {
// Get the data of the document
const data = doc.data();
console.log(data);
FCMRegistrationToken_KEY = data.token
}
})
.catch(error => {
console.log(error);
})
.finally(() => {
//more code here
// Send the notification
admin.messaging().sendToDevice(FCMRegistrationToken_KEY, payload)
.then(response => {
console.log('Notification sent successfully:', response);
})
.catch(error => {
console.log('Error sending notification:', error);
});
});
}
})
Basically when there's a new follower added, I use the userID from the context parameters to fetch the FCM token I have saved for all my users in a FCMTokens collection. After retrieving the token and creating my payload, I invoke a sendToDevice() call through admin.messaging() but it fails for some reason.
However, it fails right after that giving the following error
{
"textPayload": "Function returned undefined, expected Promise or value",
"insertId": "63c38ba0000e2c35c9c62c1d",
"resource": {
"type": "cloud_function",
"labels": {
"function_name": "sendNotificationOnFollowerCreate",
"region": "us-central1",
"project_id": "fir-eris"
}
},
"timestamp": "2023-01-15T05:14:08.928821Z",
"severity": "WARNING",
"labels": {
"execution_id": "no23uq1mg5a3",
"instance_id": "00c61b117c173e48fc2cb6c3b49f2c059090e49b7252db1b187115bd42a62998c4093f283fe06ba4ec0bf7981f108fcadb527843a8c4b3c77ec1"
},
"logName": "projects/fir-eris/logs/cloudfunctions.googleapis.com%2Fcloud-functions",
"trace": "projects/fir-eris/traces/e0d7dfae3ea1340e1ec101d16defc94b",
"receiveTimestamp": "2023-01-15T05:14:09.204309551Z"
}
I'm thoroughly confused as I really don't have that much experience with cloud functions. Can someone guide me through what's happening and what could be a potential fix for this?
Thank you.
The error that you have mentioned in the question is basically seen when a function does not or has incorrectly a return statement. The code you have for cloud function does not seem to have any return statement which will have a promise return.To make sure Cloud functions knows when your code is done, you need to either return a value from the top-level function (in the case that all work happens synchronously), or return a promise from the top-level function (in the case that some work continues after the closing } of the function).
The sendNotificationOnFollowerCreate might be aborted when the trigger function finishes because it isn't waiting for that promise.
Try adding the return similar to example below:
return DeviceToken.then(result => { const token_id = result.val();
console.log(token_id); const payload = { notification:
{ title: "New Job Request", body: `JobID ` + job_id, tag: collapseKey, } };
return admin.messaging().sendToDevice(token_id, payload)
Also check these following examples with similar implementations:
Each then should return a value firebase cloud function
Send Push notification using Cloud function for firebase
Firebase Cloud function says unreachable
Firebase Cloud push notification not being sent to device
Is there a way to send notification by identifying user rather than
device

Firebase functions cloud messaging notification not being recieved

When I send a notification from the Firebase cloud messaging console, my device receives it without a problem, but when I send it via a cloud functions, the function log says it was successfully sent but my device does not receive it. I tried switching to type script, sending the notification with different conditions but nothing works. The app is written in flutter.
My function code:
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
db.collection("users")
.where("bananas", "==", 1666).get().then(
(result) => {
if (result.size > 0) {
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
fcm.send(payload).then((response) => {
// Response is a message ID string.
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
return {success: true};
}).catch((error) => {
return {error: error.code};
});
});
}
});
response.send("Notification sent !");
functions.logger.info("Notification sent!");
return null;
});
cloud log
Any ideas?
Did you notice how your code never logs this message?
Successfully sent message
That's because both loading from Firestore, and sending messaging through Cloud Messaging are asynchronous calls. So your response.send("Notification sent !") runs before the data is ever retrieved from the database, and Cloud Functions at that point terminates your code to prevent charging after you say that you are done.
If you have asynchronous operations in your code, you need to return a promise from the top-level of your code that resolves/rejects when all asynchronous code has completed. So in your case that means the promise should only resolve once you've loaded the data from Firestore, and sent the messages.
Let's start with a simple example. Say that you want to only send a single message, no matter how many documents are in the database.
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users") // 👈 Add return here
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
const doc = result.docs[0]; // 👈 Get the first result
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
return fcm.send(payload).then((response) => { // 👈 Add return here
console.log("Successfully sent message: "+
doc.data().NotToken+ " ", response);
response.send("Notification sent !"); // 👈 Move this call here
return {success: true};
}).catch((error) => {
// TODO: Send an error back to the caller
return {error: error.code};
});
}
});
});
So the top-level code now returns the result from loading data from Firestore, and in there, we return the call from calling FCM, which then in turn returns return {success: true};. When returning promises, the results bubble up - so you can typically just keep returning the nested results.
You'll also not that we've moved the response.send into the code that runs after calling FCM, as we don't want to send a result back to the caller until the FCM call is done.
The above is the simple variant, because in reality you have multiple documents, and you are only done once all of them are done.
For that we are going to use Promise.all(), which takes an array of promises and resolves once all those promises resolve. So we're going to capture all the calls to FCM (which returns a promise) and collection them in an array, that we then pass to Promise.all().
exports.sendNotification = functions.https.onRequest((request, response) => {
const db = admin.firestore();
const fcm = admin.messaging();
return db.collection("users")
.where("bananas", "==", 1666).get().then((result) => {
if (result.size > 0) {
let promises = [];
result.forEach((doc) => {
const payload = {
token: doc.data().NotToken,
notification: {
title: "iam a notification",
body: "Yay",
icon: "https://cdn1.iconfinder.com/data/icons/logos-brands-in-colors/231/among-us-player-white-512.png",
},
};
promises.push(fcm.send(payload))
});
return Promise.al(promises).then((results) => {
console.log("Successfully sent messages");
response.send("Notification sent !");
return {success: true};
});
}
});
});
While this may be a lot to grok all at once, handling asynchronous behavior is quite well covered in the Firebase documentation on terminating functions, in this video series on Learn JavaScript Promises with Cloud Functions, and in quite a few tutorials out there - so I recommend spending some time on those to get to grips with asynchronous code.

Trouble reading data in Firebase Cloud Function

Trying to read a pushToken from a given user in the users collection (after an update operation on another collection) returns undefined
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
})
.then(r => {
getToken(idTo).then(token => {
// sendMsg...
})
}).catch(updateErr => {
console.log("updateErr: " + updateErr)
})
async function getToken(id) {
let response = "getTokenResponse"
console.log("id in getToken: " + id)
return db.collection('users').doc(id).get()
.then(user => {
console.log("user in getToken: " + user.data())
response = user.data().pushToken
})
.catch(e => {
console.log("error get userToken: " + e)
response = e
});
return response
}
return null
});
And this is from the FB console log:
-1:43:33.906 AM Function execution started
-1:43:36.799 AM Function execution took 2894 ms, finished with status: 'ok'
-1:43:43.797 AM id in getToken: Fm1RwJaVfmZoSgNEFHq4sbBgoEh1
-1:43:49.196 AM user in getToken: undefined
-1:43:49.196 AM error get userToken: TypeError: Cannot read property 'pushToken' of undefined
-1:43:49.196 AM returned token: undefined
And we can see in this screenshot from the db that the doc does exist:
Hope someone can point me to what I'm doing wrong here.
added screenshot of second example of #Renaud as deployed:
As Doug wrote in his comment, you need to "return a promise from the top level function that resolves when all the async work is complete". He also explains that very well in the official video series: https://firebase.google.com/docs/functions/video-series/ (in particular the 3 videos titled "Learn JavaScript Promises"). You should definitely watch them, highly recommended!
So, the following modifications to your code should work (untested):
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite(async (snap, context) => { // <- note the async keyword
try {
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
await db.collection('Classificados').doc(classificadoId)
.update({
aprovado: false
});
const userToSnapshot = await db.collection('users').doc(idTo).get();
const token = userToSnapshot.data().pushToken;
await sendMsg(token); // <- Here you should take extra care to correctly deal with the asynchronous character of the sendMsg operation
return null; // <-- This return is key, in order to indicate to the Cloud Function platform that all the asynchronous work is done
} catch (error) {
console.log(error);
return null;
}
});
Since you use an async function in your code, I've used the async/await syntax but we could very well write it by chaining the promises with the then() method, as shown below.
Also, I am not sure, in your case, that it adds any value to put the code that gets the token in a function (unless you want to call it from other Cloud Functions but then you should move it out of the addDenuncia Cloud Function). That's why it has been replaced by two lines of code within the main try block.
Version with chaining promises via the then() method
In this version we chain the different promises returned by the asynchronous methods with the then() method. Compared to the async/await version above, it shows very clearly what means "to return a promise from the top level function that resolves when all the asynchronous work is complete".
exports.addDenuncia = functions.firestore
.document('Denuncias/{denunciaID}')
.onWrite((snap, context) => { // <- no more async keyword
const doc = snap.after.data()
const classificadoId = doc.cid
const idTo = doc.peerId
return db.collection('Classificados').doc(classificadoId) // <- we return a promise from the top level function
.update({
aprovado: false
})
.then(() => {
return db.collection('users').doc(idTo).get();
})
.then(userToSnapshot => {
if {!userToSnapshot.exists) {
throw new Error('No document for the idTo user');
}
const token = userToSnapshot.data().pushToken;
return sendMsg(token); // Again, here we make the assumption that sendMsg is an asynchronous function
})
.catch(error => {
console.log(error);
return null;
})
});

Google Translate API and Firebase Firestore are killing each other

We're trying to write a Google Cloud Function that gets a translation from Google Translate API, and then write the results to our Firebase Firestore database. Each works alone, but together nothing works. In other words, we can get a translation from Google Translate. We can write data to Firestore. But if we try to do both, we don't get a translation back from Google Translate, and nothing is written to Firebase. We get no error messages. We've tried the code with async await and with promises. Here's the code with promises:
exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request').onUpdate((change, context) => {
if (change.after.data().word != undefined) {
const {Translate} = require('#google-cloud/translate');
const projectId = 'myProject-cd99d';
const translate = new Translate({
projectId: projectId,
});
// The text to translate
const text = change.after.data().word;
// The target language
const target = 'en';
let translationArray = []; // clear translation array
translate.translate(text, target)
.then(results => {
translation = results[0];
translationArray.push(translation);
try {
// write translation to dictionary
admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
'translationArray': translationArray,
'language': 'en',
'longLanguage': 'English'
})
.then(function() {
console.log("Translation written");
})
.catch(function(error) {
console.error(error);
});
} catch (error) {
console.error(error);
}
})
.catch(error => {
console.error('ERROR:', error);
});
}
});
Here's the same code with async await:
exports.Google_EStranslateEN = functions.firestore.document('Users/{userID}/Spanish/Translation_Request').onUpdate((change, context) => { // triggers when browser writes a request word to the database
if (change.after.data().word != undefined) {
async function getTranslation() {
try {
let translationArray = []; // clear translation array
const {Translate} = require('#google-cloud/translate');
const projectId = 'myProject-cd99d';
const translate = new Translate({
projectId: projectId,
});
// The text to translate
const text = change.after.data().word;
const options = {
to: 'en',
from: 'es',
format: 'text'
}
let [translations] = await translate.translate(text, options);
translations = Array.isArray(translations) ? translations : [translations]; // If translations is an array, leave it alone; if not, put it in an array
translationArray.push(translations[0]);
await admin.firestore().collection('Dictionaries').doc('Spanish').collection('Words').doc(text).collection('Translations').doc('English').set({
'translationArray': translationArray,
'language': 'en',
'longLanguage': 'English'
})
.then(function() {
console.log("Translation written");
})
.catch(function(error) {
console.error(error);
});
// };
} catch (error) {
console.error(error);
}
} // close getTranslation
getTranslation();
}
});
You're not returning a promise that's resolved when all the async work is complete. If you don't do that, Cloud Functions assumes that all your work is complete, and will clamp down on all resources, and any pending work will be shut down.
The promise returned by translate.translate().then().catch() is being ignored. Your nested call to admin.firestore()...set() has a similar problem. It's not sufficient to just call then() and catch() on every promise because then() and catch() both return yet another promise.
You're also unnecessarily mixing usage of try/catch with catch() on the promise. You don't need both strategies for error handling, just one or the other.
When you used await in your second example, you forced JavaScript to pause until the async work represented by the promise returned by set() was complete. This allowed your function to return only after all the work was finished, which is why it worked correctly.
You might be helped by watching my video series on use of promises and async/await in Cloud Functions. Proper handling of promises is crucial to creating a correctly working function.

FIrebase Firestore onCreate Cloud Function Event Params Undefined

I have tried following Firebase's documentation and other SO posts to access a parameter value for a cloud function I've successfully deployed.
Unfortunately I've still been receiving a
Type Error: cannot read property 'id' of undefined
I've logged event.params and it is outputting as undefined, so I understand the issue, but am unsure how, syntactically, I'm supposed to derive the param value.
Below is my js code for reference:
exports.observeCreate = functions.firestore.document('/pathOne/{id}/pathTwo/{anotherId}').onCreate(event => {
console.log(event.params);
//event prints out data but params undefined...
const data = event.data()
var id = event.params.id;
return admin.firestore().collection('path').doc(id).get().then(doc => {
const data = doc.data();
var fcmToken = data.fcmToken;
var message = {
notification: {
title: "x",
body: "x"
},
token: fcmToken
};
admin.messaging().send(message)
.then((response) => {
console.log('Successfully sent message:', response);
return;
})
.catch((error) => {
console.log('Error sending message:', error);
return;
});
return;
})
})
You're using the pre-1.0 API for the firebase-functions module, but the acutal version of it you have installed is 1.0 or later. The API changed in 1.0. Read about the changes here.
Firestore (and other types of) triggers now take a second parameter of type EventContext. This has a property called params that contains the data that used to be in event.params.
exports.observeCreate = functions.firestore.document('/pathOne/{id}/pathTwo/{anotherId}').onCreate((snapshot, context) => {
console.log(context.params);
console.log(context.params.id);
});
Please also read the documentation for the most up-to-date information about Firestore triggers.

Resources