I am currently working on a website that has a very specific requirement: the possibility to test the page performance without any external scripts. All of this company's sites need a considerable amount of scripts to be injected, as they are the company's proprietary cookie notice and analytics scripts. But since they are so many, they compromise the speed tests a lot, so there should be a way to deactivate them to know the actual performance of the site.
In previous projects, made with Gatsby.js, parameters could be passed to the URL, and depending on the parameter, a certain script would not load, or even none of them would. For example: Passing ?analytics=false makes the analytics scripts not load, ?cookie=false makes the cookie notice not load, or ?scripts=false stops all of them from loading. This way, one can simply go to Google's PageSpeed Insights and type the URL with these parameters to test the performance.
The project I'm working on, however, is a Next.js site. To load the scripts, I put them in a component called HeadScripts.tsx and load the component inside the next/head component, like so:
const App = () => {
return <>
<Head>
... any meta tag
<HeadScripts/>
</Head>
...The page component itself
</>
}
This loads them with no problem. To make them conditional to the URL params, though, my first instinct was to get the params from next/router and based on them, I check if I should load all scripts or not, like so:
const App = () => {
const router = useRouter();
const [shouldScriptsLoad, setShouldScriptsLoad] = useState(false);
useEffect(()=>{
//Example logic that checks if the url has a '?noscripts' param
!router.query.noscripts && setShouldScriptsLoad(true);
},[router.isReady])
return <>
<Head>
... any meta tag
{shouldScriptsLoad ? <HeadScripts/> : null}
</Head>
...The page component itself
</>
}
Here, I'm initializing a state as false. But, if the state changes to true during the check, the scripts don't load accordingly. If I initialize it as true then, the script will load before the check, and it will be irrelevant.
Upon checking the Gatsby site and how they do it, there is a difference that might be key to the problem: they don't have the script addition logic in a React component like Helmet. They do it in the gatsby-ssr.js file, by encapsulating each script on another script with a load listener and pushing it to <head>, to check the parameters before each one can load. Here's an example:
headComponents.push(
<script
type="text/javascript"
async={true}
dangerouslySetInnerHTML={{
__html: `
window.addEventListener('load', function() {
let hostURL = new URL(window.location.href);
let isDisabled = hostURL.searchParams.get('the parameter to be checked');
if(!isDisabled) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = 'true';
script.src = 'the script URL';
setTimeout(() => head.appendChild(script), 'append the script after some time'});}
});
`,
}}
id="the-script-id"
/>
);
exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => {
setHeadComponents(headComponents);
};
Probably this method was used because the sites are hosted on Netlify, so they could take advantage of SSR, but my site can't. Due to an infrastructure limitation, we have to build the site manually and send it to a server over FTP, so I can't use Next's getServerSideProps to figure something out from there.
So, after this long post, my question is: is there actually a way to do such thing on a Next.js site? Perhaps a way to put logic before the scripts are loaded?
Thank you all for your time, in advance. I would appreciate any help at all!
I want to load a shared worker with a user-script. The problem is the user-script is free, and has no business model for hosting a file - nor would I want to use a server, even a free one, to host one tiny file. Regardless, I tried it and I (of course) get a same origin policy error:
Uncaught SecurityError: Failed to construct 'SharedWorker': Script at
'https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js'
cannot be accessed from origin 'http://stackoverflow.com'.
There's another way to load a web worker by converting the worker function to a string and then into a Blob and loading that as the worker but I tried that too:
var sharedWorkers = {};
var startSharedWorker = function(workerFunc){
var funcString = workerFunc.toString();
var index = funcString.indexOf('{');
var funcStringClean = funcString.substring(index + 1, funcString.length - 1);
var blob = new Blob([funcStringClean], { type: "text/javascript" });
sharedWorkers.google = new SharedWorker(window.URL.createObjectURL(blob));
sharedWorkers.google.port.start();
};
And that doesn't work either. Why? Because shared workers are shared based on the location their worker file is loaded from. Since createObjectURL generates a unique file name for each use, the workers will never have the same URL and will therefore never be shared.
How can I solve this problem?
Note: I tried asking about specific solutions, but at this point I think
the best I can do is ask in a more broad manner for any
solution to the problem, since all of my attempted solutions seem
fundamentally impossible due to same origin policies or the way
URL.createObjectURL works (from the specs, it seems impossible to
alter the resulting file URL).
That being said, if my question can somehow be improved or clarified, please leave a comment.
You can use fetch(), response.blob() to create an Blob URL of type application/javascript from returned Blob; set SharedWorker() parameter to Blob URL created by URL.createObjectURL(); utilize window.open(), load event of newly opened window to define same SharedWorker previously defined at original window, attach message event to original SharedWorker at newly opened windows.
javascript was tried at console at How to clear the contents of an iFrame from another iFrame, where current Question URL should be loaded at new tab with message from opening window through worker.port.postMessage() event handler logged at console.
Opening window should also log message event when posted from newly opened window using worker.postMessage(/* message */), similarly at opening window
window.worker = void 0, window.so = void 0;
fetch("https://cdn.rawgit.com/viziionary/Nacho-Bot/master/webworker.js")
.then(response => response.blob())
.then(script => {
console.log(script);
var url = URL.createObjectURL(script);
window.worker = new SharedWorker(url);
console.log(worker);
worker.port.addEventListener("message", (e) => console.log(e.data));
worker.port.start();
window.so = window.open("https://stackoverflow.com/questions/"
+ "38810002/"
+ "how-can-i-load-a-shared-web-worker-"
+ "with-a-user-script", "_blank");
so.addEventListener("load", () => {
so.worker = worker;
so.console.log(so.worker);
so.worker.port.addEventListener("message", (e) => so.console.log(e.data));
so.worker.port.start();
so.worker.port.postMessage("hi from " + so.location.href);
});
so.addEventListener("load", () => {
worker.port.postMessage("hello from " + location.href)
})
});
At console at either tab you can then use, e.g.; at How to clear the contents of an iFrame from another iFrame worker.postMessage("hello, again") at new window of current URL How can I load a shared web worker with a user-script?, worker.port.postMessage("hi, again"); where message events attached at each window, communication between the two windows can be achieved using original SharedWorker created at initial URL.
Precondition
As you've researched and as it has been mentioned in comments,
SharedWorker's URL is subject to the Same Origin Policy.
According to this question there's no CORS support for Worker's URL.
According to this issue GM_worker support is now a WONT_FIX, and
seems close enough to impossible to implement due to changes in Firefox.
There's also a note that sandboxed Worker (as opposed to
unsafeWindow.Worker) doesn't work either.
Design
What I suppose you want to achieve is a #include * userscript that will collect some statistics or create some global UI what will appear everywhere. And thus you want to have a worker to maintain some state or statistic aggregates in runtime (which will be easy to access from every instance of user-script), and/or you want to do some computation-heavy routine (because otherwise it will slow target sites down).
In the way of any solution
The solution I want to propose is to replace SharedWorker design with an alternative.
If you want just to maintain a state in the shared worker, just use Greasemonkey storage (GM_setValue and friends). It's shared among all userscript instances (SQLite behide the scenes).
If you want to do something computation-heavy task, to it in unsafeWindow.Worker and put result back in Greasemonkey storage.
If you want to do some background computation and it must be run only by single instance, there are number of "inter-window" synchronisation libraries (mostly they use localStorage but Greasemomkey's has the same API, so it shouldn't be hard to write an adapter to it). Thus you can acquire a lock in one userscript instance and run your routines in it. Like, IWC or ByTheWay (likely used here on Stack Exchange; post about it).
Other way
I'm not sure but there may be some ingenious response spoofing, made from ServiceWorker to make SharedWorker work as you would like to. Starting point is in this answer's edit.
I am pretty sure you want a different answer, but sadly this is what it boils down to.
Browsers implement same-origin-policies to protect internet users, and although your intentions are clean, no legit browser allows you to change the origin of a sharedWorker.
All browsing contexts in a sharedWorker must share the exact same origin
host
protocol
port
You cannot hack around this issue, I've trying using iframes in addition to your methods, but non will work.
Maybe you can put it your javascript file on github and use their raw. service to get the file, this way you can have it running without much efforts.
Update
I was reading chrome updates and I remembered you asking about this.
Cross-origin service workers arrived on chrome!
To do this, add the following to the install event for the SW:
self.addEventListener('install', event => {
event.registerForeignFetch({
scopes: [self.registration.scope], // or some sub-scope
origins: ['*'] // or ['https://example.com']
});
});
Some other considerations are needed aswell, check it out:
Full link: https://developers.google.com/web/updates/2016/09/foreign-fetch?hl=en?utm_campaign=devshow_series_crossoriginserviceworkers_092316&utm_source=gdev&utm_medium=yt-desc
Yes you can! (here's how):
I don't know if it's because something has changed in the four years since this question was asked, but it is entirely possible to do exactly what the question is asking for. It's not even particularly difficult. The trick is to initialize the shared worker from a data-url that contains its code directly, rather than from a createObjectURL(blob).
This is probably most easily demonstrated by example, so here's a little userscript for stackoverflow.com that uses a shared worker to assign each stackoverflow window a unique ID number, displayed in the tab title. Note that the shared-worker code is directly included as a template string (i.e. between backtick quotes):
// ==UserScript==
// #name stackoverflow userscript shared worker example
// #namespace stackoverflow test code
// #version 1.0
// #description Demonstrate the use of shared workers created in userscript
// #icon https://stackoverflow.com/favicon.ico
// #include http*://stackoverflow.com/*
// #run-at document-start
// ==/UserScript==
(function() {
"use strict";
var port = (new SharedWorker('data:text/javascript;base64,' + btoa(
// =======================================================================================================================
// ================================================= shared worker code: =================================================
// =======================================================================================================================
// This very simple shared worker merely provides each window with a unique ID number, to be displayed in the title
`
var lastID = 0;
onconnect = function(e)
{
var port = e.source;
port.onmessage = handleMessage;
port.postMessage(["setID",++lastID]);
}
function handleMessage(e) { console.log("Message Recieved by shared worker: ",e.data); }
`
// =======================================================================================================================
// =======================================================================================================================
))).port;
port.onmessage = function(e)
{
var data = e.data, msg = data[0];
switch (msg)
{
case "setID": document.title = "#"+data[1]+": "+document.title; break;
}
}
})();
I can confirm that this is working on FireFox v79 + Tampermonkey v4.11.6117.
There are a few minor caveats:
Firstly, it might be that the page your userscript is targeting is served with a Content-Security-Policy header that explicitly restricts the sources for scripts or worker scripts (script-src or worker-src policies). In that case, the data-url with your script's content will probably be blocked, and OTOH I can't think of a way around that, unless some future GM_ function gets added to allow a userscript to override a page's CSP or change its HTTP headers, or unless the user runs their browser with an extension or browser settings to disable CSP (see e.g. Disable same origin policy in Chrome).
Secondly, userscripts can be defined to run on multiple domains, e.g. you might run the same userscript on https://amazon.com and https://amazon.co.uk. But even when created by this single userscript, shared workers obey the same-origin policy, so there should be a different instance of the shared worker that gets created for all the .com windows vs for all the .co.uk windows. Be aware of this!
Finally, some browsers may impose a size limit on how long data-urls can be, restricting the maximum length of code for the shared worker. Even if not restricted, the conversion of all the code for long, complicated shared worker to base64 and back on every window load is quite inefficient. As is the indexing of shared workers by extremely long URLs (since you connect to an existing shared worker based on matching its exact URL). So what you can do is (a) start with an initially very minimal shared worker, then use eval() to add the real (potentially much longer) code to it, in response to something like an "InitWorkerRequired" message passed to the first window that opens the worker, and (b) For added efficiency, pre-calculate the base-64 string containing the initial minimal shared-worker bootstrap code.
Here's a modified version of the above example with these two wrinkles added in (also tested and confirmed to work), that runs on both stackoverflow.com and en.wikipedia.org (just so you can verify that the different domains do indeed use separate shared worker instances):
// ==UserScript==
// #name stackoverflow & wikipedia userscript shared worker example
// #namespace stackoverflow test code
// #version 2.0
// #description Demonstrate the use of shared workers created in userscript, with code injection after creation
// #icon https://stackoverflow.com/favicon.ico
// #include http*://stackoverflow.com/*
// #include http*://en.wikipedia.org/*
// #run-at document-end
// ==/UserScript==
(function() {
"use strict";
// Minimal bootstrap code used to first create a shared worker (commented out because we actually use a pre-encoded base64 string created from a minified version of this code):
/*
// ==================================================================================================================================
{
let x = [];
onconnect = function(e)
{
var p = e.source;
x.push(e);
p.postMessage(["InitWorkerRequired"]);
p.onmessage = function(e) // Expects only 1 kind of message: the init code. So we don't actually check for any other sort of message, and page script therefore mustn't send any other sort of message until init has been confirmed.
{
(0,eval)(e.data[1]); // (0,eval) is an indirect call to eval(), which therefore executes in global scope (rather than the scope of this function). See http://perfectionkills.com/global-eval-what-are-the-options/ or https://stackoverflow.com/questions/19357978/indirect-eval-call-in-strict-mode
while(e = x.shift()) onconnect(e); // This calls the NEW onconnect function, that the eval() above just (re-)defined. Note that unless windows are opened in very quick succession, x should only have one entry.
}
}
}
// ==================================================================================================================================
*/
// Actual code that we want the shared worker to execute. Can be as long as we like!
// Note that it must replace the onconnect handler defined by the minimal bootstrap worker code.
var workerCode =
// ==================================================================================================================================
`
"use strict"; // NOTE: because this code is evaluated by eval(), the presence of "use strict"; here will cause it to be evaluated in it's own scope just below the global scope, instead of in the global scope directly. Practically this shouldn't matter, though: it's rather like enclosing the whole code in (function(){...})();
var lastID = 0;
onconnect = function(e) // MUST set onconnect here; bootstrap method relies on this!
{
var port = e.source;
port.onmessage = handleMessage;
port.postMessage(["WorkerConnected",++lastID]); // As well as providing a page with it's ID, the "WorkerConnected" message indicates to a page that the worker has been initialized, so it may be posted messages other than "InitializeWorkerCode"
}
function handleMessage(e)
{
var data = e.data;
if (data[0]==="InitializeWorkerCode") return; // If two (or more) windows are opened very quickly, "InitWorkerRequired" may get posted to BOTH, and the second response will then arrive at an already-initialized worker, so must check for and ignore it here.
// ...
console.log("Message Received by shared worker: ",e.data); // For this simple example worker, there's actually nothing to do here
}
`;
// ==================================================================================================================================
// Use a base64 string encoding minified version of the minimal bootstrap code in the comments above, i.e.
// btoa('{let x=[];onconnect=function(e){var p=e.source;x.push(e);p.postMessage(["InitWorkerRequired"]);p.onmessage=function(e){(0,eval)(e.data[1]);while(e=x.shift()) onconnect(e);}}}');
// NOTE: If there's any chance the page might be using more than one shared worker based on this "bootstrap" method, insert a comment with some identification or name for the worker into the minified, base64 code, so that different shared workers get unique data-URLs (and hence don't incorrectly share worker instances).
var port = (new SharedWorker('data:text/javascript;base64,e2xldCB4PVtdO29uY29ubmVjdD1mdW5jdGlvbihlKXt2YXIgcD1lLnNvdXJjZTt4LnB1c2goZSk7cC5wb3N0TWVzc2FnZShbIkluaXRXb3JrZXJSZXF1aXJlZCJdKTtwLm9ubWVzc2FnZT1mdW5jdGlvbihlKXsoMCxldmFsKShlLmRhdGFbMV0pO3doaWxlKGU9eC5zaGlmdCgpKSBvbmNvbm5lY3QoZSk7fX19')).port;
port.onmessage = function(e)
{
var data = e.data, msg = data[0];
switch (msg)
{
case "WorkerConnected": document.title = "#"+data[1]+": "+document.title; break;
case "InitWorkerRequired": port.postMessage(["InitializeWorkerCode",workerCode]); break;
}
}
})();
I would like to create a custom plugin for tracking events in my JavaScript Application using Google Analytics Measurement Tool (GA MT), but I am a newbie and not sure how to write such plugin.
My idea about the plugin:
it should have defined all types of events I am going to track (i.e. starting of an application, button clicked, 1st, 2nd, ... slide entered, etc)
if I understand how GA MT works correctly, I will need to specify an event hit for each custom event (see more)
part of the hit parameters (url) is shared (such as version, client ID, tracking ID...)
the other part of the url is custom, so I will store the differences in various functions inside of the plugin
these functions will be later called i.e. on button clicked, on goToNextSlide etc, which will send a hit to GA.
This is an example of my plugin:
(function( $ ) {
var $_document = $(document);
// Shared hit parameters
var hit = 'https://www.google-analytics.com/collect?';
hit += 'v=1'; // Version.
hit += '&t=pageview'; // Pageview hit type.
hit += '&tid=UA-XXXXXX-Y'; // Tracking ID / Property ID.
hit += '&cid=555'; // Client ID.
/* Application opened */
function gacAppOpened() {
console.log('gacAppOpened');
hit += '&dp=%2Fslide-1'; // Page.
httpGetRequest(hit);
}
/* Slide-2 entered */
function gacSlide2() {
console.log('gacSlide2');
hit += '&dp=%2Fslide-2'; // Page.
httpGetRequest(hit);
}
function httpGetRequest( theUrl )
{
var req = new XMLHttpRequest();
req.open("POST", theUrl, true);
req.send(null);
}
}( jQuery ));
This is how I load the plugin (gaCustom.js) and my common JS file (app.js)
<script src="js/gaCustom.js"></script>
<script src="js/app.js"></script>
When trying to reach to my function from inside app.js, I got error (not a function)
goToDefault: function() { // loads first page of my app,
// a hit should be sent to GA about app started
...
gacAppOpened();
... // render template
},
So I am wrong somehow in defining the plugin and using it. I also tried few other attempts, but all of them failed.
I would appreciate to hear whether my approach is good or wrong and what to improve as I am a newbie and would like to do this correctly.
I am using the following Google Apps Script code to display content in a custom sidebar of my spreadsheet while the script runs:
function test() {
var sidebarContent = '1<br>';
updateSidebar(sidebarContent);
sidebarContent += '2<br>';
updateSidebar(sidebarContent);
sidebarContent += '3';
updateSidebar(sidebarContent);
}
function updateSidebar(content) {
var html = HtmlService.createHtmlOutput(content)
.setSandboxMode(HtmlService.SandboxMode.IFRAME)
.setTitle('Sidebar')
.setWidth(250);
SpreadsheetApp.getUi().showSidebar(html);
}
It works, but each time the updateSidebar() function runs, the sidebar blinks when loading in the new content.
How can I program this to update the content of the sidebar more efficiently, thus removing the blink?
I'm assuming that SpreadsheetApp.getUi().showSidebar(html); should really only be run once, at the beginning, and the subsequent updates to the content should be handled by Javascript in a .js file.
But I don't know how to get the sidebarContent variable from Javascript code running client-side in the user's browser.
Also, I know this must be possible, because I just saw this post on the Google Apps Developer Blog today about an app that uses a custom sidebar, and the .gif towards the end of the article shows a nicely-animated sidebar that's being updated in real-time.
I believe the solution for this situation is to actually handle the flow of the server-side script from the client-side. That is the only way I can think of right now to pass data to the client side from the server without re-generating the HTML.
What I mean by this is that you would want to make the calls to the server-side functions from the client, and have them return a response as a success handler to the client. This means that each action that needs to be logged will need to be made into its own function.
Ill show you a quick example of what I mean.
Lets say your server-side GAS code looked like this:
function actionOne(){
...insert code here...
return true;
}
function actionTwo(){
...insert code here...
return true;
}
And so on for as many actions need to be executed.
Now, for your .html file, at the bottom you would have javascript looking something like this:
<script>
callActionOne();
function callActionOne(){
google.script.run.withSuccessHandler(callActionTwo).actionOne();
}
function callActionTwo(){
...update html as necessary to indicate that the first action has completed...
google.script.run.withSuccessHandler(actionsComplete).actionTwo();
}
function actionsComplete(){
..update html to indicate script is complete...
}
</script>
It is a bit more complex than is ideal, and you might need to use the CacheService to store some data in between actions, but it should help you with your problem.
Let me know if you have any questions or if this doesn't fit your needs.
In emails that we send to users, we include links to Angular app like the following:
http://example.com/#!/mypage
We've noticed that some email clients or browsers, for one reason or another, upon click direct the user to this instead:
http://example.com/#%21/mypage
Angular then throws the following error:
Uncaught Error: [$location:ihshprfx] Invalid url "http://example.com/#%21/mypage", missing hash prefix "#!".
http://errors.angularjs.org/1.3.0-beta.10/$location/ihshprfx
We are using $locationProvider.hashPrefix('!');. I'm trying to find a way to detect instances where $location is /#%21/ rather than /#!/ and then redirect properly, but I can't find a way to detect and/or get Angular to do this. What is the proper way to do this?
Ended up finding a better answer here:
Adding a hash prefix at the config phase if it's missing
Using $locationChangeStart didn't work because angular threw the error during initialization, so $locationChangeStart was never tripped.
Instead, I went with the following approach:
<head>
<!-- Change #%21 to #! on first load -->
<script type="text/javascript">
var loc = window.location.href;
if (loc.indexOf('#%21') > -1 && loc.indexOf('#!') === -1 ) {
window.location.href = loc.replace("#%21", "#!");
}
</script>
<!-- More stuff ... -->
</head>
<body>...
This (a) allows me to rewrite the URL before we ever hit Angular, and (b) makes sure we only rewrite it the first time the app is loaded, rather than any time there's a location change--just in case at some point in the future we deliberately write a change that includes a #%21.
Might not be the most elegant way to go about solving your problem, but you could catch the $locationChangeStart event and then conditionally redirect the user with the $location service based on what the nature of the URL is.
For example:
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
// ...
});