Saving progress data with redux-saga when the user leaves the page - javascript

I'm building a system where a user is scored on the percentage of a video they have watched. If they leave/refresh / close the page I need to post their scoring data to an API endpoint.
I use the beforeunload event to fire the action when the user changes their location.
The action is handled using redux-saga.
I can see the action is being dispatched but it seems that the window closes before the call is made to the endpoint (chrome dev tools shows the call but with a status of canceled).
Is there any way to reliably ensure that the call is made to the endpoint? Surely there are a lot of sites (most notably e-learning ones) that handle this kind of behavior consistently, so there must be a solution.
Component:
componentDidMount() {
window.addEventListener('beforeunload', this.onUnload);
}
componentWillUnmount() {
window.removeEventListener('beforeunload', this.onUnload);
}
onUnload() {
this.props.postScore(params);
}
Any help is greatly appreciated.

If redux store is your app state, which is about to be go kaput, this is a rare time you have to bypass the store.
Just synchronously read the store and directly post to the api.
But even this is only saving the progress when the browser fires "unload".
If the page becomes unresponsive, or the browser simply crashes, the handler and api call will never execute.
A simple tactic would be to continually update progress every x seconds

Related

How to call API only when user reload/leave site from the browser alert and not on click of cancel?

I am trying to do an API call when the user is trying to close/reload the browser/tab. I don't want to call the API if the user clicks on cancel. I followed JavaScript, browsers, window close - send an AJAX request or run a script on window closing, but it didn't solve my issue. I followed catching beforeunload confirmation canceled? for differentiating between confirm and cancel. I have no idea how to make the API call when the user reloads/closes the browser and not to call the API when user clicks on cancel. I followed JavaScript, browsers, window close - send an AJAX request or run a script on window closing and tried like
For showing alert on reload or close the tab
<script>
window.addEventListener("onbeforeunload", function(evt){
evt.preventDefault()
const string = '';
evt.returnValue = string;
return string;
})
</script>
and on click of cancel, nothing should happen. If the user is forcefully closing the browser or reloading, the API should be called
<script type="module">
import lifecycle from 'https://cdn.rawgit.com/GoogleChromeLabs/page-lifecycle/0.1.1/dist/lifecycle.mjs';
lifecycle.addEventListener('statechange', function(event) {
if (event.originalEvent === 'visibilitychange' && event.newState === 'hidden') {
var URL = "https://api.com/" //url;
var data = '' //payload;
navigator.sendBeacon(URL, data);
}
});
</script>
But it's not happening. Any help is appreciated. Thanks
Your problem is happening because you're using beforeunload to present a prompt.
I can see that you're handling the beforeunload event properly, so you must already be aware that browser vendors have deliberately limited the ability of script authors to do custom stuff when the user wants to leave the page. This is to prevent abuse.
Part of that limitation is that you don't get to find out what the user decides to do. And there will not be any clever workarounds, either. Once you tell the browser to present the beforeunload prompt, you lose all your power. If the user clicks the Okay button (i.e. decides to leave the page), the browser will refuse to run any more of your code.
Presenting the prompt creates a fork in the road that you are prevented from observing. So, put a laser tripwire there instead of a fork:
window.addEventListener("onbeforeunload", function(evt) {
navigator.sendBeacon(url, payload)
})
This is guaranteed to run when the user actually leaves the page, and only when the user actually leaves the page. But, you sacrifice the ability to try to talk the user out of leaving. You can't have it both ways.
You can't always get what you want, but if you try, sometimes you just might find you get what you need. -- The Rolling Stones
I can only think of one way to accomplish what you need, but it requires help from the server. This is not an option for most people (usually because the beacon goes to a third-party analytics provider who won't do this), but I'm including it here for completeness.
before the beforeunload handler returns, fire a beacon message that says "user is maybe leaving the page"
after firing that beacon, and still before returning, set up a document-wide mousemove handler that fires a second beacon message that says "the user is still here" (and also de-registers itself)
return false to present the prompt
modify your server so that it will reconcile these two events after some kind of delay:
if the server receives beacon 1 and then also receives beacon 2 (within some reasonably short time-frame, e.g. 5 minutes), it means the user tried to leave but then changed their mind, and so the server should delete the record of beacon 1
if the server receives beacon 1 but doesn't receive beacon 2 within the time-frame, then it means the user really did leave, and so the server would rewrite the previous beacon datapoint to say "user actually departed"; you wouldn't need to actually write beacon 2 to your datastore
(Or, depending on expected traffic and your infrastructure, maybe the server just holds the beacon 1 datapoint in RAM for the 5 minutes and commits it to your datastore only if beacon 2 never shows up. Or you could write both beacons to the database and then have a different process reconcile the beacons later. The outcome is identical, but they have different performance characteristics and resource requirements.)
P.S.: Never use "URL" (all caps) as a variable name in javascript. "URL" is actually a useful web API, so if you use that exact variable name, you're clobbering a useful ability. It's just like if you did let navigator = 'Henry'. Yes, it will execute without error, but it shadows a useful native capability.

Closing Chrome window not sending data with sendBeacon in unload event handler

I am trying to send data when the window closes to prevent 2 people from editing and overwriting each others data. Currently I am using a sendBeacon within a unload event handler.
FireFox:
Refresh: Works
Back button: Works
Close window: Works
Chrome:
Refresh: Works
Back button: Works
Close window: Doesn't work
Here is my code
function sendDataOnClose(edit,trans){
var url = "../../save.php"; //This has a post request handler and works properly with other functions for saving data
const data = JSON.stringify
({
"translations": trans,
"edit": edit
});
navigator.sendBeacon(url, data);
}
function handleClose(){
if(edit){
console.log("sending a false when edit is: "+ edit)
sendDataOnClose(false, translations);
}
}
window.addEventListener('unload', handleClose);
The latest sendBeacon documentation on MDN, states "The navigator.sendBeacon() method asynchronously sends a small amount of data over HTTP to a web server. It’s intended to be used in combination with the visibilitychange event (but not with the unload and beforeunload events)."
To use the visibilitychange event like suggested, you could
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
navigator.sendBeacon(handleClose);
}
});
I have experienced similar issues with trying send data on the unload event. Is the user base all on desktop? Mobile devices don't reliably fire the unload event. The Page Lifecycle API provides the visibility change event and the pagehide events which could be used together to get closer to your desired result.
The Page Lifecycle API attempts to solve this problem by:
Introducing and standardizing the concept of lifecycle states on the web.
Defining new, system-initiated states that allow browsers to limit the resources that can be consumed by hidden or inactive tabs.
Creating new APIs and events that allow web developers to respond to transitions to and from these new system-initiated states.
source
The issue you are experiencing is likely more of an issue without how browsers suspend pages or discard them entirely. Unfortunately, browsers are not unified on how they do this, and to add to the complexity there is different behavior on desktop vs. mobile.
There are several threads that dive in deeper to this issue if you are interested. Until browsers standardize on this, I'm not sure there is an easy answer, such as "use x event".
Issue filed on Page Visibility
Issue on MDN's strints about sendBeacon
Thanks to hackers, many other things are removed for security reasons.
I noticed your question had the PHP tag as well; I will give you a, not good idea, but a functional one. Avoid on-close-page handles even JavaScript or frameworks just post with JavaScript a table database where you store time() and an target id then if timeout is maybe more than 30 sec you set then you will remove from table that stuff, and you will know that page isn't still working (translation: use a server "online users" idea (bad but necessary one like anything generates lots of traffic in an app).
Using these in JavaScript in the side client is bad idea and you open gates for bad guys that will exploit your app.

How to know when a page redirect or refresh action is cancelled, Javascript

I am currently working on a page where a preloader is shown to the user onPageNavigating, and when the user cancels the navigation from the browser, I would like to remove the preloader.
What i have tried.
Create a countDown timer
Start timer onPageNavigating
If timer elapse, assume navigation was cancelled
Hide preloader
But the problem with this method is that the time of navigation may vary based on network speed which makes it not feasible for my use-case.
What are other options to implement or is there an available browser API for this? because I can't find anything related via search.
EDIT:
Similar use-case where issue persists:
For example visit:
https://developer.android.com/guide or any link within its domain and notice the horizontal preloader at the top of the page. When you start navigation or refresh from the page you see the preloader shown but the challenge persist as I explained. Cancel the refresh immediately and the preloader would still be visible which isn't meant to be so.
Edit 2:
Maybe rephrasing the question will help.
When i said onPageNavigating i actually mean Javascript's beforeunload event which is triggered when a user clicks on a link or initiates a page refresh.
window.addEventListener('beforeunload', function (e) {
showPreloader();
// ...At this point the preloader is visible
});
Because the beforeunload event can be cancelled. How can you tell when it is cancelled so the preloader can be hidden?
Imaginary event:
window.addEventListener('beforeunloadcancelled', function (e) {
hidePreloader();
// ...This is what i am looking for
});
TL;DR
There is one approach to receive or emulate beforeunloadcancelled event in browsers using only browser APIs right now. But it doesn't work due to bugs in browsers which still haven't been fixed
Below I'll describe the main approach, why it doesn't work and what workarounds may be.
Approach based on browser APIs only
The main idea of emulating beforeunloadcancelled event is to get event that navigation request for next page has been cancelled by the user. You cannot control such kind of requests in the main browser context but, fortunately, you can do so from the service worker context.
If your service worker subscribes to fetch event, you will be able to work with all navigation requests your application makes:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
}
});
According to the specification each Request object has property signal. This signal represents a signal object that allows you to communicate with a DOM request (such as a Fetch) and abort it if required via an AbortController object.
You can subscribe to the abort event to get notified when request is aborted by the browser or via an AbortController. In that listener you can post message to the parent window with the information that navigation request has been canceled which actually means beforeunloadcancelled event:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
evt.request.signal.addEventListener('abort', async () => {
// exit early if we don't have an access to the client (e.g. if it's cross-origin)
if (!evt.clientId) return;
// get the client
const client = await self.clients.get(evt.clientId);
// exit early if we don't get the client (e.g., if it's closed)
if (!client) return;
// send a message to the client
client.postMessage({
msg: 'navigation request has been canceled'
});
})
}
});
Unfortunately, there are bugs that those abort events aren't reflected in service worker. Neither Chromium or Firefox have implemented properly sending the AbortSignal through to FetchEvent.request.signal. Please take a look:
https://bugs.chromium.org/p/chromium/issues/detail?id=823697
https://bugzilla.mozilla.org/show_bug.cgi?id=1394102
So this approach doesn't work at the moment.
You can find more information on that topic here:
Abort Controller for Service Worker
https://developers.google.com/web/updates/2017/09/abortable-fetch#in_a_service_worker
https://github.com/w3c/ServiceWorker/issues/1544
https://github.com/w3c/ServiceWorker/issues/1284
Possible workarounds
Detect of cancelled requests on the server side
If you have access to the server, you can detect that the navigation request has been cancelled on the server side.
Just send a notification to the client side in the event the navigation request is aborted. You can use WebSockets to send notifications. To make sure the notification is sent to the correct client some unique cookies based client Id may be used.
Timers based approaches
As you mentioned in the question, one possible workaround is to use some kind of timers:
window.addEventListener('beforeunload', () => {
showPreloader();
setTimeout(() => {
hidePreloader(); // we're assuming navigation has been cancelled
}, TIMEOUT);
});
To make this approach more precise you can choose the TIMEOUT value based on the current page load speed (which can be found in PerformanceNavigationTiming API):
const [initialNavigation] = window.performance.getEntriesByType('navigation');
const TIMEOUT = initialNavigation.duration * TIMEOUT_RATIO;
Track navigation request load duration with service worker
There is a way to track navigation request progress via service worker. You can send notification to the parent window when the navigation request is done. If navigation does not occur immediately after this notification, it means that navigation is effectively canceled and the preloader can be hidden:
// in Service Worker global scope
self.addEventListener('fetch', evt => {
if (evt.request.mode === 'navigate') {
console.log(`browser is navigating to the ${evt.request.url} resource now`);
evt.respondWith(fetch(evt.request).then(response => {
sendHidePreloaderNotification();
return response;
}));
}
});
You can always combine last two options developing more stable approach.

How to notify without websocket

I want to integrate a simple notification system in my react application. I want to notify for example:
- new post (when the user post the system need time to transcode the media attached and the publication)
- missing settings (the user need to compile some information)
- interesting posts etc..
There is a simple way to add a websocket, like socket.io, to a reactjs app with an aws lambda backend?
All the notification not need to be read in real time, maybe an ajax call every 2 minutes can solve my problem, but, in this case, someone can help me avoid ajax call if the app isn't used(like if the app remain opened in a foreground tab...)
componentDidMount() {
this.liveUpdate()
setInterval(this.liveUpdate, 120000);
}
liveUpdate() {
axios.get(endpoint.posts+'/live/', cfg)
.then(res => {
// ...
});
}
This code is in the footer component, the call happen every 120 seconds, but the call will still happen also if a user leave the application opened in the browser and not use it, this on a lambda backend mean a waste of money.
There are 3 main ways of notifying that I can think of at the moment...
Long polling (using ajax etc)
Websocket
Push Notification
Push (though) requires permission from the user

Callback, which fires on every user action

I would like to implement a timeout functionality in an AngularJS web app. Whenever a user does anything, a callback sets the timer to 10 minutes. When this timer expires on the client-side, a timeout request is sent to the server.
How can I implement a callback which fires from every user action? Maybe register onclick and onkeydown listener on the whole page/window?
According to the angular docs for $rootScope:
If you want to be notified whenever $digest() is called, you can
register a watchExpression function with $watch() with no listener.
I am not sure if $digest is called on every user action but it might be a good place to start.
FYI - How I implement a User time-out is as follows:
Authenticate user via server side API
Have API store the user in a cache with sliding expiration and pass back a session token
Have each secured API end-point require a session token and use this to fetch the User from the cache thus resetting the expiration timer.
If the User does not exist in the cache return a 403 forbidden error which your client code handles, presumably by sending the user back to login page. Actually I return a custom 403 that has a specific 'User Timeout' code and message so my client can handle the time-out gracefully.
This should work fine for most Single Page Apps because pretty much anything the user does causes a state change and most state changes involve a call to the server to fetch or save stuff. It misses out on minor user actions but in I have found that the real world use of this technique has sufficient resolution to be effective.
Looks like hackedbychinese.github.io/ng-idle does what I need

Categories

Resources