fix: isolate tracker credentials and count static download intent

This commit is contained in:
2026-09-30 03:40:35 +00:00
parent 1693b20d58
commit 7a59d9b405
4 changed files with 89 additions and 12 deletions
+9 -1
View File
@@ -9,8 +9,16 @@ Automatic Umami tracking is disabled, including history and click hooks. One
manual pageview is sent per loaded document. The before-send hook reconstructs manual pageview is sent per loaded document. The before-send hook reconstructs
a strict projection: website, hostname, static title `Alta Export Player`, static path a strict projection: website, hostname, static title `Alta Export Player`, static path
`/`, empty referrer, validated ordinary browser screen/language. It drops `/`, empty referrer, validated ordinary browser screen/language. It drops
identity, named/custom events, tags, arbitrary properties, user title, URL, identity, unknown/custom events, tags, arbitrary properties, user title, URL,
query/fragment, filenames, upload contents, forms and application state. query/fragment, filenames, upload contents, forms and application state.
Only fixed download controls emit fixed event names/properties: `#releaseAction` → `download_desktop_app` (`tool: alta-export-player`, `format: desktop`). Native desktop/update modes are excluded.
These are click intents, not successful-download reports. A capture listener does
not read the link URL or filename, cancel navigation, or await analytics. Unknown events and
caller-provided event data are discarded; whitelisted event data is reconstructed.
The hook is installed before creating/loading the external script, auto tracking
is disabled, and an unavailable bootstrap produces no tracker GET/POST. The script
uses anonymous CORS and no-referrer; collection fetch credentials are omit.
No application data is read. Both script requests and collection requests must No application data is read. Both script requests and collection requests must
have no Cookie, Authorization or Referer headers. Tracker credentials are omit. have no Cookie, Authorization or Referer headers. Tracker credentials are omit.
+23 -6
View File
@@ -1,4 +1,4 @@
/* Production pageviews only. Never pass application/browser DOM data to Umami. */ /* Static production pageviews/download intent. No application data to Umami. */
(() => { (() => {
'use strict'; 'use strict';
const hostname = "player.nenasets.com"; const hostname = "player.nenasets.com";
@@ -11,18 +11,25 @@
&& paths.includes(window.location.pathname); && paths.includes(window.location.pathname);
if (!eligible() || window.__altaPageviewLoaded) return; if (!eligible() || window.__altaPageviewLoaded) return;
window.__altaPageviewLoaded = true; window.__altaPageviewLoaded = true;
// Rebuild, don't spread the input: no identity, event properties, referrer, const controls = {releaseAction: 'download_desktop_app'};
const events = {download_desktop_app: {tool: 'alta-export-player', format: 'desktop'}};
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
// Rebuild, don't spread the input: no identity, arbitrary properties, referrer,
// customer title, query, fragment, form, filename or upload data can survive. // customer title, query, fragment, form, filename or upload data can survive.
window.altaSafePageview = (type, payload) => { window.altaSafePageview = (type, payload) => {
if (!eligible() || type !== 'event' || !payload || payload.name) return null; if (!eligible() || type !== 'event' || !payload) return null;
return { website, hostname, url, title, referrer: '', if (payload.name && (typeof payload.name !== 'string' || !own(events, payload.name))) return null;
screen: /^\d{1,5}x\d{1,5}$/.test(payload.screen) ? payload.screen : undefined, const safe = { website, hostname, url, title, referrer: '',
language: /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(payload.language) ? payload.language : undefined }; screen: typeof payload.screen === 'string' && /^\d{1,5}x\d{1,5}$/.test(payload.screen) ? payload.screen : undefined,
language: typeof payload.language === 'string' && /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(payload.language) ? payload.language : undefined };
if (payload.name) { safe.name = payload.name; safe.data = {...events[payload.name]}; }
return safe;
}; };
const tracker = document.createElement('script'); const tracker = document.createElement('script');
tracker.src = 'https://umami.nenasets.com/script.js'; tracker.src = 'https://umami.nenasets.com/script.js';
tracker.defer = true; tracker.defer = true;
tracker.referrerPolicy = 'no-referrer'; tracker.referrerPolicy = 'no-referrer';
tracker.crossOrigin = 'anonymous';
for (const [key, value] of Object.entries({ for (const [key, value] of Object.entries({
'website-id': website, domains: hostname, 'auto-track': 'false', 'website-id': website, domains: hostname, 'auto-track': 'false',
'exclude-search': 'true', 'exclude-hash': 'true', 'exclude-search': 'true', 'exclude-hash': 'true',
@@ -35,5 +42,15 @@
window.umami.track(); window.umami.track();
} }
}; };
// Capture intent once, before application handlers; never cancel/await navigation.
// The web download anchor and native desktop/update behavior stay unchanged.
document.addEventListener('click', event => {
if (!eligible() || window.webavpNative || event.button !== 0) return;
const control = event.target.closest?.('a[id]');
if (!control || control.disabled || control.getAttribute('aria-disabled') === 'true'
|| !own(controls, control.id) || !window.umami) return;
const name = controls[control.id];
try { window.umami.track(name, {...events[name]}); } catch (_) { /* Analytics is optional. */ }
}, true);
document.head.appendChild(tracker); document.head.appendChild(tracker);
})(); })();
+1 -1
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer"> <meta name="referrer" content="no-referrer">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://umami.nenasets.com/script.js; style-src 'self' 'unsafe-inline'; connect-src 'self' https://umami.nenasets.com/api/send; media-src blob:; img-src 'self' data:;"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://umami.nenasets.com/script.js; style-src 'self' 'unsafe-inline'; connect-src 'self' https://umami.nenasets.com/api/send; media-src blob:; img-src 'self' data:;">
<script src="/static/privacy-pageview.js?v=1" defer></script> <script src="/static/privacy-pageview.js?v=2" defer></script>
<title>Alta Video Player</title> <title>Alta Video Player</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%23121826'/><polygon points='38,25 38,75 78,50' fill='%23006ED7'/></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect rx='20' width='100' height='100' fill='%23121826'/><polygon points='38,25 38,75 78,50' fill='%23006ED7'/></svg>">
<script src="/static/jszip.min.js"></script> <script src="/static/jszip.min.js"></script>
+56 -4
View File
@@ -10,13 +10,15 @@ const site = player ? 'dc1b14a6-d1ba-4802-b895-553b3c81c677' : 'e00bb594-4c6e-44
const host = player ? 'player.nenasets.com' : 'tools.nenasets.com'; const host = player ? 'player.nenasets.com' : 'tools.nenasets.com';
const route = player ? '/' : '/private/onvif/'; const route = player ? '/' : '/private/onvif/';
function boot(url) { function boot(url) {
const scripts = [], tracked = []; const scripts = [], tracked = [], listeners = [];
const window = { location: new URL(url), umami: {track: () => tracked.push(true)} }; const window = { location: new URL(url), umami: {track: (...args) => tracked.push(args)} };
const document = { createElement: () => ({setAttribute(k,v) { this[k] = v; }}), head: {appendChild: s => scripts.push(s)} }; const document = { addEventListener: (type, fn, capture) => listeners.push({type, fn, capture}),
createElement: () => ({setAttribute(k,v) { this[k] = v; }}),
head: {appendChild: s => { assert.equal(typeof window.altaSafePageview, 'function', 'hook installed before load'); scripts.push(s); }} };
const context = vm.createContext({window, document}); const context = vm.createContext({window, document});
const source = fs.readFileSync(path.join(root, 'static/privacy-pageview.js'), 'utf8'); const source = fs.readFileSync(path.join(root, 'static/privacy-pageview.js'), 'utf8');
vm.runInContext(source, context); vm.runInContext(source, context);
return {window, scripts, tracked, context, source}; return {window, scripts, tracked, listeners, context, source};
} }
test('production pageviews use one manually loaded no-referrer tracker', () => { test('production pageviews use one manually loaded no-referrer tracker', () => {
const b = boot('https://' + host + route + '?customer=PRIVATE#PRIVATE'); const b = boot('https://' + host + route + '?customer=PRIVATE#PRIVATE');
@@ -24,6 +26,7 @@ test('production pageviews use one manually loaded no-referrer tracker', () => {
const s = b.scripts[0]; const s = b.scripts[0];
assert.equal(s.src, 'https://umami.nenasets.com/script.js'); assert.equal(s.src, 'https://umami.nenasets.com/script.js');
assert.equal(s.referrerPolicy, 'no-referrer'); assert.equal(s.referrerPolicy, 'no-referrer');
assert.equal(s.crossOrigin, 'anonymous');
assert.equal(s['data-auto-track'], 'false'); assert.equal(s['data-auto-track'], 'false');
assert.equal(s['data-exclude-search'], 'true'); assert.equal(s['data-exclude-search'], 'true');
assert.equal(s['data-exclude-hash'], 'true'); assert.equal(s['data-exclude-hash'], 'true');
@@ -57,6 +60,55 @@ test('fail closed on DEV, previous, unknown, API, port, alternate host and HTTP
'https://' + host + '/customer-name', 'https://' + host + '/api/verify-cert']; 'https://' + host + '/customer-name', 'https://' + host + '/api/verify-cert'];
for (const url of excluded) assert.equal(boot(url).scripts.length, 0, url); for (const url of excluded) assert.equal(boot(url).scripts.length, 0, url);
}); });
const downloads = player ? [['releaseAction', 'download_desktop_app', {tool:'alta-export-player',format:'desktop'}]]
: [['download-template', 'download_csv_template', {tool:'onvif',format:'csv',kind:'template'}],
['download-report', 'download_csv_report', {tool:'onvif',format:'csv',kind:'report'}]];
test('static download clicks count once without changing native navigation or reading user data', () => {
const b = boot('https://' + host + route);
b.scripts[0].onload();
vm.runInContext(b.source, b.context);
assert.equal(b.listeners.length, 1);
assert.equal(b.listeners[0].type, 'click');
assert.equal(b.listeners[0].capture, true);
for (const [id, name, data] of downloads) {
const control = {id,tagName:'A',disabled:false,getAttribute:()=>null,
form:{checkValidity:()=>false},closest:()=>control};
const event = {target:{closest:()=>control},button:0,preventDefault:()=>assert.fail('navigation intercepted')};
const n=b.tracked.length;
b.listeners[0].fn(event);
assert.equal(b.tracked.length,n+1);
assert.equal(b.tracked.at(-1)[0],name);
assert.deepEqual(JSON.parse(JSON.stringify(b.tracked.at(-1)[1])),data);
const safe=b.window.altaSafePageview('event',{name,data:{url:'PRIVATE',filename:'PRIVATE'},id:'PRIVATE',tag:'PRIVATE'});
assert.equal(safe.name,name);
assert.deepEqual(JSON.parse(JSON.stringify(safe.data)),data);
assert.ok(!JSON.stringify(safe).includes('PRIVATE'));
control.disabled=true;b.listeners[0].fn(event);assert.equal(b.tracked.length,n+1);
}
b.window.location=new URL('https://'+host+'/private/previous/');
const n=b.tracked.length;
b.listeners[0].fn({target:{closest:()=>({id:downloads[0][0],tagName:'A'})},button:0});
assert.equal(b.tracked.length,n);
});
test('analytics missing or throwing does not prevent download clicks', () => {
const b=boot('https://'+host+route);
const e={target:{closest:()=>({id:downloads[0][0],tagName:'A',getAttribute:()=>null})},button:0};
delete b.window.umami;assert.doesNotThrow(()=>b.listeners[0].fn(e));
b.window.umami={track:()=>{throw Error('offline');}};assert.doesNotThrow(()=>b.listeners[0].fn(e));
});
test('known events use an own-property whitelist, native desktop modes are excluded', () => {
const b=boot('https://'+host+route);
for(const name of ['__proto__','constructor','toString','PRIVATE', {toString:()=>downloads[0][1]}])
assert.equal(b.window.altaSafePageview('event',{name}),null);
if(player){
b.window.webavpNative={checkForUpdates:()=>{}};
const n=b.tracked.length;
b.listeners[0].fn({target:{closest:()=>({id:'releaseAction',tagName:'A',getAttribute:()=>null})},button:0});
assert.equal(b.tracked.length,n);
}
});
test('only native application document routes are eligible', () => { test('only native application document routes are eligible', () => {
for(const suffix of (player ? [''] : ['', 'upload', 'results', 'process', 'clear'])) for(const suffix of (player ? [''] : ['', 'upload', 'results', 'process', 'clear']))
assert.equal(boot('https://' + host + route + suffix).scripts.length, 1, suffix); assert.equal(boot('https://' + host + route + suffix).scripts.length, 1, suffix);