+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
index da3c99ef8..fdcc85958 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/koala73/worldmonitor/stargazers)
[](https://github.com/koala73/worldmonitor/network/members)
-[](https://opensource.org/licenses/MIT)
+[](https://www.gnu.org/licenses/agpl-3.0)
[](https://www.typescriptlang.org/)
[](https://github.com/koala73/worldmonitor/commits/main)
[](https://github.com/koala73/worldmonitor/releases/latest)
@@ -925,6 +925,7 @@ The `.env.example` file documents every variable with descriptions and registrat
| **Geopolitical** | `ACLED_ACCESS_TOKEN`, `CLOUDFLARE_API_TOKEN`, `NASA_FIRMS_API_KEY` | Free for researchers |
| **Relay** | `WS_RELAY_URL`, `VITE_WS_RELAY_URL`, `OPENSKY_CLIENT_ID/SECRET` | Self-hosted |
| **UI** | `VITE_VARIANT`, `VITE_MAP_INTERACTION_MODE` (`flat` or `3d`, default `3d`) | N/A |
+| **Observability** | `VITE_SENTRY_DSN` (optional, empty disables reporting) | N/A |
See [`.env.example`](./.env.example) for the complete list with registration links.
@@ -1101,7 +1102,7 @@ If you find World Monitor useful:
## License
-MIT License — see [LICENSE](LICENSE) for details.
+GNU Affero General Public License v3.0 (AGPL-3.0) — see [LICENSE](LICENSE) for details.
---
diff --git a/package.json b/package.json
index adddc5ec8..425bdf074 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "world-monitor",
"private": true,
"version": "2.4.0",
+ "license": "AGPL-3.0-only",
"type": "module",
"scripts": {
"lint:md": "markdownlint-cli2 '**/*.md'",
diff --git a/src/components/Map.ts b/src/components/Map.ts
index ab926527e..3001dba2e 100644
--- a/src/components/Map.ts
+++ b/src/components/Map.ts
@@ -581,9 +581,9 @@ export class MapComponent {
} else {
// Geopolitical variant legend
legend.innerHTML = `
- ${escapeHtml(t('popups.hotspot.levels.high').toUpperCase())}
- ${escapeHtml(t('popups.hotspot.levels.elevated').toUpperCase())}
- ${escapeHtml(t('popups.monitoring').toUpperCase())}
+ ${escapeHtml((t('popups.hotspot.levels.high') ?? 'HIGH').toUpperCase())}
+ ${escapeHtml((t('popups.hotspot.levels.elevated') ?? 'ELEVATED').toUpperCase())}
+ ${escapeHtml((t('popups.monitoring') ?? 'MONITORING').toUpperCase())}
⚔${escapeHtml(t('modals.search.types.conflict').toUpperCase())}
●${escapeHtml(t('modals.search.types.earthquake').toUpperCase())}
⚠APT
@@ -826,6 +826,7 @@ export class MapComponent {
}
// Simple viewBox matching container - keeps SVG and overlays aligned
+ if (!this.svg) return;
this.svg.attr('viewBox', `0 0 ${width} ${height}`);
// CRITICAL: Always refresh d3 selections from actual DOM to prevent stale references
diff --git a/src/main.ts b/src/main.ts
index c8dd2ce7a..77a3faba6 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -4,14 +4,16 @@ import * as Sentry from '@sentry/browser';
import { inject } from '@vercel/analytics';
import { App } from './App';
+const sentryDsn = import.meta.env.VITE_SENTRY_DSN?.trim();
+
// Initialize Sentry error tracking (early as possible)
Sentry.init({
- dsn: 'https://afc9a1c85c6ba49f8464a43f8de74ccd@o4509927897890816.ingest.us.sentry.io/4510906342113280',
+ dsn: sentryDsn || undefined,
release: `worldmonitor@${__APP_VERSION__}`,
environment: location.hostname === 'worldmonitor.app' ? 'production'
: location.hostname.includes('vercel.app') ? 'preview'
: 'development',
- enabled: !location.hostname.startsWith('localhost') && !('__TAURI_INTERNALS__' in window),
+ enabled: Boolean(sentryDsn) && !location.hostname.startsWith('localhost') && !('__TAURI_INTERNALS__' in window),
sendDefaultPii: true,
tracesSampleRate: 0.1,
ignoreErrors: [
@@ -39,17 +41,24 @@ Sentry.init({
/vc_text_indicators_context/,
/Program failed to link: null/,
/too much recursion/,
+ /zaloJSV2/,
+ /Java bridge method invocation error/,
+ /Could not compile fragment shader/,
+ /can't redefine non-configurable property/,
+ /Can't find variable: (CONFIG|currentInset)/,
+ /invalid origin/,
+ /\.data\.split is not a function/,
],
beforeSend(event) {
const msg = event.exception?.values?.[0]?.value ?? '';
if (msg.length <= 3 && /^[a-zA-Z_$]+$/.test(msg)) return null;
const frames = event.exception?.values?.[0]?.stacktrace?.frames ?? [];
- // Suppress module-import failures only when originating from browser extensions
+ // Suppress module-import failures only when originating from browser extensions.
if (/Importing a module script failed/.test(msg)) {
if (frames.some(f => /^(chrome|moz)-extension:/.test(f.filename ?? ''))) return null;
}
// Suppress maplibre internal null-access crashes (light, placement) only when stack is in map chunk
- if (/this\.style\._layers|this\.light is null|can't access property "type", \w+ is undefined|Cannot read properties of null \(reading '(id|type)'\)/.test(msg)) {
+ if (/this\.style\._layers|this\.light is null|can't access property "(type|setFilter)", \w+ is (null|undefined)|Cannot read properties of null \(reading '(id|type|setFilter)'\)|null is not an object \(evaluating '(E\.|this\.style)/.test(msg)) {
if (frames.some(f => /\/map-[A-Za-z0-9]+\.js/.test(f.filename ?? ''))) return null;
}
return event;
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index dbb4c627d..c3d60d2b1 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,3 +1,11 @@
///
declare const __APP_VERSION__: string;
+
+interface ImportMetaEnv {
+ readonly VITE_SENTRY_DSN?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}