Preventing ad blockers from blocking Umami, using a Cloudflare worker [solution] #1026
Replies: 7 comments 7 replies
-
For anyone wondering, probably could be possible to achieve the same behavior using Next.js API routes, for Next.js based projects. Not need to use an external third-party provider in these cases. I came here from Google facing the same problem and my website is made with Next.js, but it's pass midnight in my timezone, and I think I'll try to update it tomorrow. I'll bring updates here in the following days if I achieve it. |
Beta Was this translation helpful? Give feedback.
-
Thanks for sharing, I made a few changes to the code to fix the CROS problem😀 const ScriptName = '/whatever.js';
const Endpoint = '/foo/bar';
const UmamiUrl = 'https://your.umami.url';
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
};
const ScriptWithoutExtension = ScriptName.replace('.js', '')
addEventListener('fetch', event => {
event.passThroughOnException();
event.respondWith(handleRequest(event));
})
async function handleRequest(event) {
const pathname = new URL(event.request.url).pathname
const [baseUri, ...extensions] = pathname.split('.')
const clientIP = event.request.headers.get("CF-Connecting-IP")
if (baseUri.endsWith(ScriptWithoutExtension)) {
return getScript(event, extensions)
} else if (pathname.endsWith(Endpoint)) {
return postData(event)
}
return new Response(null, {status: 404})
}
async function getScript(event, extensions) {
let response = await caches.default.match(event.request);
if (!response) {
response = await fetch(UmamiUrl +"/umami.js");
var js = await response.text();
js = js.replace("/api/collect", Endpoint);
response = new Response(js, {
headers: {
...response.headers,
...corsHeaders,
'Access-Control-Allow-Headers': response.headers.get('Access-Control-Request-Headers'),
},
})
event.waitUntil(caches.default.put(event.request, response.clone()));
}
return response;
}
async function postData(event) {
const request = new Request(event.request);
request.headers.delete('cookie');
response = await fetch(UmamiUrl +"/api/collect", request);
var js = await response.text();
response = new Response(js, {
headers: {
...response.headers,
...corsHeaders,
'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers'),
},
});
return response;
} |
Beta Was this translation helpful? Give feedback.
-
So finally, I achieved it with Next.js API routes. I'll share here my probably improvable implementation. Also includes the CORS improvements from @keven1024. This is working on my website, deployed on Vercel. Umami is deployed in Vercel too, if that matters. Just some notes:
// file: /pages/api/umami/[uri].ts
import axios from 'axios'
import { NextApiRequest, NextApiResponse } from 'next'
const scriptName = 'whatever.js'
const endpointName = 'endpoint-name'
const umamiUrl = 'https://your.umami.url'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Max-Age': '86400',
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { uri } = req.query
if ((uri as string).endsWith(scriptName)) {
return getScript(req, res)
} else if ((uri as string).endsWith(endpointName)) {
return postData(req, res)
}
res.status(404).send(null)
}
async function getScript(req: NextApiRequest, res: NextApiResponse) {
const response = await axios.get(umamiUrl + '/umami.js', {
headers: {
...req.headers,
...corsHeaders,
'accept-encoding': 'gzip',
host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80)
} as unknown as Record<string, string>,
decompress: true,
})
const originalScript = await response.data
const obfuscatedScript = originalScript.replace(
new RegExp('/api/collect', 'g'),
`/${endpointName}`,
)
res.status(response.status ?? 200).send(obfuscatedScript)
}
async function postData(req: NextApiRequest, res: NextApiResponse) {
const response = await axios.post(umamiUrl + '/api/collect', req.body, {
headers: {
...req.headers,
...corsHeaders,
host: null, // not removing host header will result in a weird SSL error that leads to a 500 code (EPROTO SSL alert number 80)
} as unknown as Record<string, string>,
})
res.status(response.status ?? 201).send(response.data)
} And in my <script
async
defer
data-website-id={UMAMI_ANALYTICS_ID}
src={'https://my-website-domain.com/api/umami/whatever.js')} // Careful to use your site url, not umami, as we want to call the Nextjs API route to download the script
/> |
Beta Was this translation helpful? Give feedback.
-
The author added 'TRACKER_SCRIPT_NAME' in version 1.26.0 to modify the name of JS script without the help of cloudflare workers. |
Beta Was this translation helpful? Give feedback.
-
I was testing this with Cloudflare. While the script is working when the page load, but it is stopped by when I use it to track clicks on external links, uBlock Origin blocks those Beacon API requests. |
Beta Was this translation helpful? Give feedback.
-
After #1257 is merged, this can be solved natively by using the variables |
Beta Was this translation helpful? Give feedback.
-
Used this but while it gets all page impressions, it now tracks just a fourth of unique visitors than before. Any idea what that's about? |
Beta Was this translation helpful? Give feedback.
-
Hi, I thought I'd share this because I am sure others have the same problem - uBlock Origin and others blocking Umami.
The solution I am using involves Cloudflare workers, which have an incredible free tier. So all you need to do is create a Cloudflare worker with the following code:
Then you need to tweak the Umami snippet for your site:
That's it. Just customize the names of the script and the endpoint for the POST requests with unique names and no ad blocker should be able to block Umami, because the script name, endpoint path, and even the content of the script are unique.
Hope this helps others :)
Beta Was this translation helpful? Give feedback.
All reactions