feat: add privacy-safe production player pageviews

This commit is contained in:
2026-09-30 02:58:33 +00:00
parent 2c93670aaf
commit 1693b20d58
5 changed files with 144 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
# Production pageview privacy contract
`static/privacy-pageview.js` loads the current Umami script only on HTTPS,
exact hostname `player.nenasets.com`, default port, and known application document routes.
It never loads on loopback, desktop protocol, DEV, previous, rollback, API,
admin, download, or unknown routes. Website: `dc1b14a6-d1ba-4802-b895-553b3c81c677`.
Automatic Umami tracking is disabled, including history and click hooks. One
manual pageview is sent per loaded document. The before-send hook reconstructs
a strict projection: website, hostname, static title `Alta Export Player`, static path
`/`, empty referrer, validated ordinary browser screen/language. It drops
identity, named/custom events, tags, arbitrary properties, user title, URL,
query/fragment, filenames, upload contents, forms and application state.
No application data is read. Both script requests and collection requests must
have no Cookie, Authorization or Referer headers. Tracker credentials are omit.
The only CSP additions are the exact script and endpoint paths:
`https://umami.nenasets.com/script.js` in script-src and
`https://umami.nenasets.com/api/send` in connect-src. Existing restrictions stay.
No gateway/auth/deployment/registry configuration is changed by this commit.
## Repeatable gates
- `node --test tests/analytics.test.js`: hostname/path/protocol/port allowlist,
single pageview, automatic tracking off, reconstruction and identity/event denial.
- `node scripts/check.js`: native utility/server checks plus analytics suite.
- Root is the only native application document route. No synthetic media fixture
is provided; local acceptance exercised shell/layout controls, not footage playback.
Built-image Chromium desktop/mobile acceptance and request evidence live outside
this source checkout in the rollout project worker reports. The audited real
tracker was served locally and its POSTs intercepted with a204 sink, not sent to
production Umami. This does not prove organic ingestion. ONVIF acceptance uses
unchanged documented real bundled catalogue records, independent local signing
secrets and a test-only employee verifier; it does not prove a Google login.
No push or deployment is part of this commit. Inspect CI and deployment bindings
before publishing; the coordinator owns exact-image promotion and rollback.
+1
View File
@@ -27,6 +27,7 @@ for (const file of ['electron/main.js', 'electron/preload.js', 'static/webavp-ut
run(process.execPath, ['--check', file]);
}
run(process.execPath, ['tests/webavp-utils.test.js']);
run(process.execPath, ['--test', 'tests/analytics.test.js']);
const html = fs.readFileSync(path.join(ROOT_DIR, 'templates/index.html'), 'utf8');
const required = [
+39
View File
@@ -0,0 +1,39 @@
/* Production pageviews only. Never pass application/browser DOM data to Umami. */
(() => {
'use strict';
const hostname = "player.nenasets.com";
const website = "dc1b14a6-d1ba-4802-b895-553b3c81c677";
const url = "/";
const title = "Alta Export Player";
const paths = ["/"];
const eligible = () => window.location.protocol === 'https:'
&& window.location.hostname === hostname && window.location.port === ''
&& paths.includes(window.location.pathname);
if (!eligible() || window.__altaPageviewLoaded) return;
window.__altaPageviewLoaded = true;
// Rebuild, don't spread the input: no identity, event properties, referrer,
// customer title, query, fragment, form, filename or upload data can survive.
window.altaSafePageview = (type, payload) => {
if (!eligible() || type !== 'event' || !payload || payload.name) return null;
return { website, hostname, url, title, referrer: '',
screen: /^\d{1,5}x\d{1,5}$/.test(payload.screen) ? payload.screen : undefined,
language: /^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(payload.language) ? payload.language : undefined };
};
const tracker = document.createElement('script');
tracker.src = 'https://umami.nenasets.com/script.js';
tracker.defer = true;
tracker.referrerPolicy = 'no-referrer';
for (const [key, value] of Object.entries({
'website-id': website, domains: hostname, 'auto-track': 'false',
'exclude-search': 'true', 'exclude-hash': 'true',
'fetch-credentials': 'omit', 'before-send': 'altaSafePageview'
})) tracker.setAttribute('data-' + key, value);
let sent = false;
tracker.onload = () => {
if (!sent && eligible() && window.umami) {
sent = true;
window.umami.track();
}
};
document.head.appendChild(tracker);
})();
+3 -1
View File
@@ -3,7 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; media-src blob:; img-src 'self' data:;">
<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:;">
<script src="/static/privacy-pageview.js?v=1" defer></script>
<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>">
<script src="/static/jszip.min.js"></script>
+63
View File
@@ -0,0 +1,63 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const root = path.resolve(__dirname, '..');
const player = fs.existsSync(path.join(root, 'package.json'));
const site = player ? 'dc1b14a6-d1ba-4802-b895-553b3c81c677' : 'e00bb594-4c6e-4471-911d-be3f7fdd9fae';
const host = player ? 'player.nenasets.com' : 'tools.nenasets.com';
const route = player ? '/' : '/private/onvif/';
function boot(url) {
const scripts = [], tracked = [];
const window = { location: new URL(url), umami: {track: () => tracked.push(true)} };
const document = { createElement: () => ({setAttribute(k,v) { this[k] = v; }}), head: {appendChild: s => scripts.push(s)} };
const context = vm.createContext({window, document});
const source = fs.readFileSync(path.join(root, 'static/privacy-pageview.js'), 'utf8');
vm.runInContext(source, context);
return {window, scripts, tracked, context, source};
}
test('production pageviews use one manually loaded no-referrer tracker', () => {
const b = boot('https://' + host + route + '?customer=PRIVATE#PRIVATE');
assert.equal(b.scripts.length, 1);
const s = b.scripts[0];
assert.equal(s.src, 'https://umami.nenasets.com/script.js');
assert.equal(s.referrerPolicy, 'no-referrer');
assert.equal(s['data-auto-track'], 'false');
assert.equal(s['data-exclude-search'], 'true');
assert.equal(s['data-exclude-hash'], 'true');
assert.equal(s['data-fetch-credentials'], 'omit');
assert.equal(s['data-domains'], host);
assert.equal(s['data-website-id'], site);
s.onload(); s.onload();
vm.runInContext(b.source, b.context);
assert.equal(b.scripts.length, 1);
assert.equal(b.tracked.length, 1);
});
test('before-send rebuilds a minimal static pageview; no identity, events or arbitrary data', () => {
const b = boot('https://' + host + route);
const guard = b.window[b.scripts[0]['data-before-send']];
const dirty = {website:'PRIVATE',url:'PRIVATE',title:'PRIVATE',hostname:'PRIVATE',referrer:'PRIVATE',name:'PRIVATE',data:{filename:'PRIVATE'},id:'PRIVATE',tag:'PRIVATE',screen:'1920x1080',language:'en-US'};
assert.equal(guard('identify', dirty), null);
assert.equal(guard('event', dirty), null);
delete dirty.name;
const safe = JSON.parse(JSON.stringify(guard('event', dirty)));
assert.deepEqual(safe, {website:site,hostname:host,url:route,title:player?'Alta Export Player':'ONVIF Tool',referrer:'',screen:'1920x1080',language:'en-US'});
assert.ok(!JSON.stringify(safe).includes('PRIVATE'));
b.window.location = new URL('https://' + host + '/private/previous/');
assert.equal(guard('event', dirty), null);
});
test('fail closed on DEV, previous, unknown, API, port, alternate host and HTTP routes', () => {
const excluded = ['http://' + host + route, 'https://' + host + ':8443' + route,
'https://dev.' + host + route, 'https://127.0.0.1' + route,
'https://' + host + '/private/onvif-dev/', 'https://' + host + '/private/onvif-previous/',
'https://' + host + '/private/onvif/previous/', 'https://' + host + '/private/onvif/api/lookup',
'https://' + host + '/private/onvif/download', 'https://' + host + '/private/onvif/admin',
'https://' + host + '/customer-name', 'https://' + host + '/api/verify-cert'];
for (const url of excluded) assert.equal(boot(url).scripts.length, 0, url);
});
test('only native application document routes are eligible', () => {
for(const suffix of (player ? [''] : ['', 'upload', 'results', 'process', 'clear']))
assert.equal(boot('https://' + host + route + suffix).scripts.length, 1, suffix);
});