Custom logger in Firestore to create statistics on operations? - javascript

How can I sniff all traffic in Firestore JS SDK?
To explain in more detail, the following code enables verbose logging into console.
import { setLogLevel } from 'firebase/firestore'
setLogLevel('debug')
Now, everytime a network communication happens, in the console there are outputs like:
[2022-09-28T12:00:21.878Z] #firebase/firestore: Firestore (9.6.10): Connection WebChannel received:
{"documentChange":{"document":{"name":"projects/mu-project/databases/(default)/documents/my_collection/sHM7OvtpEygAKQBawxha","fields":{ ...
Is it possible to subscribe to such logs with a custom callback, instead of printing it in console, so I can analyze the communication?
Something like:
import { setLogLevel, setLogger } from 'firebase/firestore'
setLogLevel('debug')
setLogger((message, payload) => {
// custom stuff
})
It is intended to create statistics on writes and reads, that are intended to be used on FE in realtime.

It's possible in that you can build your own version of the SDK and make it do whatever you want. It's open source.
Otherwise, no, it's not possible.
You're probably better off instrumenting your own application code to make it capture the things it's doing with Firestore.

I have found onLog function which can be used to register a callback on every produced log message. However, it does not solve the question precisely, because the function does not intercept the logging, and therefore, cannot mute the output to the console. It will produce a lot of debug messages which is not desired in the production deployment.
import { onLog, setLogLevel } from 'firebase/app'
setLogLevel('debug') // must be used to track the traffic
onLog((args) => {
console.log('Custom log callback: ', args)
}, {level: 'debug'})
Also note that onLog must be called after the Firestore component is initialized.

Related

Service Worker determine number of clients

We've got a SaaS app that's PWA capable and we're using Workbox for the grunt work. Up to now we've been following the tried and trusted recipe of displaying an update is available banner to the users, prompting them to update their web app.
Viewing usage data (via Sentry.io) we've noticed that most users simply seem to ignore the update banner and continue with the version they're on.
So we're looking at trying something different. This is to perform the update automatically when they change the route in the web app (when we know there's an update available).
Testing this shows that it does work. However there's a side-effect, and that is if they've got web app open in multiple tabs, then all the tabs get updated. This could be problematic for users' if they've got an un-saved form open in one of the tabs in the background - they'll potentially loose their work.
This happens during this piece of code:
// app shell page, created lifecycle hook
document.addEventListener('swUpdated', this.SetPwaRegistration, { once: true })
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (this.refreshing) {
return
}
this.refreshing = true
window.location.reload()
})
// app shell page, method in methods collection
SetPwaRegistration (event) {
// call mutation to pass the registration object to Vuex
this.PWA_REGISTRATION_SET({ pwaRegistration: event.detail })
}
// main.js
router.afterEach((to, from) => {
// retrieve the registration object from Vuex
const pwaRegistration = app.$store.getters.pwaRegistration
if (pwaRegistration) {
pwaRegistration.waiting.postMessage('skipWaiting')
}
})
the above code is from our Vue.js app code-base, the this.refreshing is set to false by default in the data property collection.
What I'd like to know if whether it is possible to determine if the Service Worker has only one client under it's control (i.e. the web app is only open in 1 browser tab), and if this is the case, the auto-update can happen without potential issues. If there's more than one client, then we'll display the update banner as usual.
As a brief update to this, I've come across code examples similar to this:
self.clients.matchAll().then(clients => {
const clientCount = clients.length
// store count of client in Vuex
})
Which looks like an option (i.e. count how many clients there are, store this in Vuex store), I'm just not sure where it should be used.
If you want to orchestrate the reload from entirely within the service worker, you can effectively do that by using the WindowClient interface to programmatically navigate to whatever the current URL is, which is roughly equivalent to a reload.
The two things to keep in in mind that navigate() will only work on WindowClients (not Workers) and that you can only call it if the service worker controls the WindowClient.
Putting that together, here's something to try:
// This can be run, e.g., in a `message` handler in your SW:
self.clients.matchAll({
// These options are actually the defaults, but just
// to be explicit:
includeUncontrolled: false,
type: 'window',
}).then((clients) => {
if (clients.length === 1) {
clients[0].navigate(clients[0].url);
}
})

Authentication with Firebase and context API in react give me a warning. Is this the right approach?

I am trying to set up an authentication on my web application in React using firebase and Context API.
I am using Context API since as long as I understood I cannot save my jwt token in local storage in order to not be vulnerable to XSS attack and at the moment I do not want to use Redux.
in my App.js I have:
const {setUserInfo} = useContext(userInfoContext);
useEffect(() => {
auth.onAuthStateChanged(user => {
if (user) {
setUserInfo({jwtToken: user.za});
} else {
setUserInfo({jwtToken: null});
}
console.log(user);
});
}, [setUserInfo]);
The methos "auth.onAuthStateChanged" is triggered every time I logged in or I logged out using firebase.auth.
The compiler tell me that to eliminate the warning I should have "[setUserInfo]" instead of "[]". However, doing as he say, the method setUserInfo is executed twice. There is a better way to achieve the result without a warning?
Your problem is that you don't clean up your effect when it is recomputed. As soon as you add setUserInfo to the dependency array, the effect is executed whenever its value changes. This means that you could potentially register many auth.onAuthStateChanged if the value of setUserInfo changes.
auth.onAuthStateChanged returns an unsubscribe function. You can simply return this function inside your effect, which will make react execute the unsubscribe function whenever the hook is executed again and prevent you from having multiple active listeners. I suggest you read more about this topic here.

Auth0 client is null in Vue SPA on page refresh

I have a Vue SPA based on one of Auth0's quickstart apps (https://github.com/auth0-samples/auth0-vue-samples). Everything works fine out of the box, but as soon as I try using the Auth0 client in my component code I run into problems. I followed the "Calling an API" tutorial (https://auth0.com/docs/quickstart/spa/vuejs/02-calling-an-api), which unhelpfully only shows how to call an API using a button. What I want to do is trigger an authenticated call to my API on initial page load so that I can ensure certain data exists in my own API (or create it if it does not). This seems like it should be pretty straightforward. I just throw this code in my created hook of my Vue component:
await this.$auth.getTokenSilently().then((authToken) => {
// reach out to my API using authToken
});
This actually works fine if the app hot reloads from my npm dev server, it reaches out to my API, which authorizes the request using the token, and sends back the correct data. The problem is when I manually reload the page, which causes this:
Uncaught (in promise) TypeError: Cannot read property 'getTokenSilently' of null
at Vue.getTokenSilently (authWrapper.js?de49:65)
at _callee$ (App.vue?234e:49)
Inside the authWrapper.js file (where the Auth0 client lives), the function call is here:
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
}
When I debug the call, "auth0Client" doesn't exist, which is why it's failing. What I can't understand is the correct way to ensure it does exist before I attempt to make the call. There's nothing in the samples that indicates the right way to do this. I tried putting my component code in different components and different Vue lifecycle hooks (created, beforeMount, mounted, etc), all with the same result. The client becomes available after 800 ms or so, but not when this code executes.
This is clearly a timing problem, but it's not clear to me how to tell my component code to sit and wait until this.auth0Client is non-null without doing something horrible and hacky like a setInterval.
I figured out a workaround for now, which I'll add as an answer in case anyone else has this issue, although it's not really the answer I want. Per the authGuard, you can use the exported "instance" from the authWrapper and watch its "loading" flag before executing your code that depends on the auth0Client being ready, like this:
import { getInstance } from "./auth/authWrapper";
// ... Vue component:
created() {
this.init(this.doSomethingWithAuth0Client);
},
methods: {
init(fn) {
// have to do this nonsense to make sure auth0Client is ready
var instance = getInstance();
instance.$watch("loading", loading => {
if (loading === false) {
fn(instance);
}
});
},
async doSomethingWithAuth0Client(instance) {
await instance.getTokenSilently().then((authToken) => {
// do authorized API calls with auth0 authToken here
});
}
}
It's hardly ideal, but it does work.

Having trouble with persisting React state using localStorage with a SSR (Next.js) app

The app I'm making has customizable settings. I'd like to load default settings, then when a user makes any changes, the custom settings will be stored in localStorage. Now the next time a user comes back, we'll load the setting from their localStorage.
I'm using React context like so...
const SettingsContextProvider = (props: any) => {
const [settings, setSettings] = useState(getSettings());
useEffect(() => {
localStorage.setItem('settings', JSON.stringify(settings))
}, [settings]);
return (...some jsx...);
}
Then getSettings()...
getSettings() {
// get from local storage
if (process.browser) {
const localSettings = localStorage.getItem('settings');
if (localSettings) return JSON.parse(localSettings );
}
// fall back to default settings
return "Something else";
}
The issue I'm having is that the server side load (on the node side), we don't have local storage (which is why I check for process.browser), so it falls back to default settings. THEN when it gets to the browser, it seems to call getSettings() again, in which case we DO have local storage and it loads from there.
That works, but then I get an error:
Text content did not match. Server: "Something else" Client: "Something custom"
So I understand that the server isn't matching the client and it's upset about it. I get WHY it's happening, but I have NO IDEA how to fix it. Maybe I need to implement reducers or use cookies?
Has anyone come across this?
I'm more than happy to share my full repo code if it'll help.
I'm fairly comfortable with react but have pretty much NEVER used react hooks in my own code.
However, I feel like I wouldn't put the updating of the localStorage into a useEffect function like that (and I could be totally wrong about that).
So, first, I would check that that useEffect function is being called when you're expecting it to be.
And then, I would write a function
const updateSettings = (newSettings) => {
localStorage.setItem('settings', JSON.stringify(newSettings))
setSettings(newSettings);
}
And use that updateSettings function to pass down to components.
BUT, I could be totally off there, like I said I don't use react hooks and only have a theoretical understanding.

Access the error response without modifying the Amplify library

I am using the AWS Amplify library to handle the login on my React app. This works by wrapping the entire app in a HOC, and I have added custom UIs for the login screens. By default Amplify displays error messages from Cognito in a toast but I would like to display these as plain text within my custom UI.
I have raised this issue on the Amplify repo and have been informed there is no way to customise the error messages but it is currently a feature request. In the meantime, I think there must be a workaround.
There are 3 ways I believe I might be able to access the error message in React:
When an error is received Amplify sets it in the state of 'Authenticator', which is the parent component (created by the HOC) of my custom UI. Without modifying the actual library I can't pass this down as props or pass it into context/redux but is there some hack to be able to access this?
The request to Cognito is handled by a fetch request generated from the library but I can see the response in the console. A 400 response with the error message in the body of the response. Is there a way to set up some kind of event listener to get the error message off the http response?
The library also generates a toast when an error message is returned. I am currently passing the HOC a css theme (display: none) to hide this. Is there a way to listen for the creation of the toast component and get the message off the span element that lays inside?
Any advice would be greatly appreciated.
Matthew
I found two ways to solve this issue.
Firstly, if you are only interested in ‘signIn’, ‘signUp’ or ‘signOut’ events. You can use the ‘Hub’ utility to listen for an 'auth' event before you attempt an authentication action like signIn and then set the error message to state. Something like this:
import { Hub } from "aws-amplify";
handleLogin = e => {
Hub.listen("auth", res => {
const errorMsg = res.payload.data.message ? res.payload.data.message : null;
this.setState(prevState => ({...prevState, errMsg: errorMsg}));
})
this.signIn(e);
}
Alternatively, if like me you want the error messages off confirm sign in or forgot password events. You can use the Auth API and catch the error message. You have to figure out what needs to be passed into each method but the docs are quite informative. Here's an example for the confirm sign in:
import { Auth } from 'aws-amplify';
confrimSignIn = () => {
Auth.confirmSignIn(this.props.authData, this.inputs.code, 'SMS_MFA')
.then(() => this.changeState('signedIn'))
.catch(err => this.setState({errMsg: err.message}));;
}

Categories

Resources