Security and privacy
<mapvx-website> runs inside your page, with your page’s origin and permissions. Besides rendering
the directory, loading it has effects that your Content Security Policy (CSP), your privacy notice
and your consent flow need to account for:
- The visitor’s browser connects to the MapVX API and to map tile hosts.
- As soon as
main.jsloads, the bundle loads Google Analytics 4 for MapVX’s own property, which then receives events from your page. - The bundled maps SDK reports its errors to MapVX.
- The browser asks for location access (and, on iOS, motion access) only when the visitor uses a location feature.
- The component stores one preference in
localStorage.
This page describes the published version 1.6.1.
Content Security Policy
If your site sends a Content-Security-Policy header, it must allow what the component loads at
runtime:
| Directive | What the component needs |
|---|---|
script-src | main.js, a module script, from your origin or a CDN, and the Google Analytics loader that the bundle appends to <head> (https://www.googletagmanager.com). |
style-src | styles.css, plus the <style> elements that the component injects into <head> at runtime. Allow those with a nonce or with 'unsafe-inline'. |
style-src-attr | 'unsafe-inline': some Font Awesome icons, the current-location marker and the formatting of the Deals + Events text use inline style attributes, and nonces don’t apply to attributes. |
img-src | data: (icons inlined in styles.css), blob: (used by the map library in some browsers), and the hosts that serve place images. |
connect-src | The MapVX API, map tiles, sprites and label glyphs, error reports and Google Analytics hits. |
worker-src | blob:: the map’s worker is embedded in main.js and started from a blob: URL. |
font-src | Nothing: the component loads no web fonts. Map labels are fetched through connect-src. |
Without worker-src blob: (or child-src blob:), the map stays empty, and the only sign is a CSP
violation in the browser console. There is no separate worker file that you could self-host
instead.
The Deals + Events section of a place profile shows formatted text (HTML) from your MapVX
content. Without 'unsafe-inline' in script-src, inline event handlers in that text, such as
onclick attributes, don’t run.
The two policies below were tested with the published 1.6.1 bundle in Chromium, in map view with location tracking on and on a place profile with Deals + Events and a route from the current location, and produced no violations. For the general CSP reference, see MDN .
Allowlist policy
This policy is for a self-hosted bundle (main.js and styles.css served from your own origin,
see Self-hosting), with a nonce for the injected styles. The example
builds the header value in Node.js; the directives are the same for any server.
import { randomBytes } from 'node:crypto'
// A new nonce for every response
const nonce = randomBytes(16).toString('base64')
const directives = {
'default-src': ["'self'"],
'script-src': ["'self'", 'https://www.googletagmanager.com'],
'style-src': ["'self'", `'nonce-${nonce}'`],
'style-src-attr': ["'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'blob:', 'https:'],
'connect-src': [
"'self'",
// MapVX API
'https://public-api.mapvx.com',
'https://api.mapvx.com',
// Map tiles, sprites and label glyphs
'https://tiles.mapvx.com',
'https://indoorequals.mapvx.com',
'https://api.maptiler.com',
'https://lazarillo.app',
// Error reports
'https://sentry.mapvx.app',
// Google Analytics
'https://*.google-analytics.com',
'https://*.analytics.google.com',
'https://*.googletagmanager.com',
// 3D model decoders (3D maps only)
'https://mapvx-glb-assets.s3.us-east-1.amazonaws.com',
'https://www.gstatic.com',
],
'worker-src': ["'self'", 'blob:'],
// For your own fonts: the component loads none
'font-src': ["'self'", 'data:'],
'object-src': ["'none'"],
'base-uri': ["'self'"],
}
const csp = Object.entries(directives)
.map(([name, sources]) => `${name} ${sources.join(' ')}`)
.join('; ')
// Send it with the HTML response:
// res.setHeader('Content-Security-Policy', csp)The HTML of the same response carries the same nonce (NONCE_VALUE below):
<head>
<link rel="stylesheet" href="/mapvx-website/1.6.1/styles.css" />
<style nonce="NONCE_VALUE">
mapvx-website {
display: block;
width: 100%;
}
</style>
</head>
<body>
<!-- Angular stamps this nonce on the <style> elements it injects -->
<mapvx-website
ngCspNonce="NONCE_VALUE"
api-key="YOUR_API_KEY"
institution-id="YOUR_INSTITUTION_ID"
></mapvx-website>
<script type="module" src="/mapvx-website/1.6.1/main.js"></script>
</body>- Loading from a CDN: add its origin,
https://cdn.jsdelivr.netorhttps://unpkg.com, to bothscript-srcandstyle-src, and pin the exact version you tested (see CDN). - Map hosts are data-driven: the map style that the API returns for each institution names the
tile, sprite and glyph hosts. The list covers the hosts seen in MapVX map styles, plus
indoorequals.mapvx.com, a MapVX indoor tile host that the tests didn’t reach. If the console reports a blocked map request, add that host. - Place images come from the hosts that your institution’s content uses; the placeholder shown
for places without images is on
images.ctfassets.net. That’s whyimg-srcallowshttps:. Narrow it only if you know every image host. - 3D decoders: the last two
connect-srchosts serve decoders for 3D models, which the SDK loads only for 3D maps. The component’s map is 2D, so they aren’t contacted in normal use; they were part of the tested policy. - Add your own site’s sources (scripts, fonts, analytics) to the same directives.
Nonce for injected styles
At runtime, Angular adds <style> elements with the component’s styles to <head>, and Font
Awesome adds one more (<style id="fa-auto-css">). To allow them without 'unsafe-inline', pass
Angular a nonce with the ngCspNonce attribute:
- Generate a new random nonce for every response and add it to
style-srcas'nonce-…'. - Put
ngCspNoncewith the same value on<mapvx-website>, or on any other element inside<body>(not on<body>itself), in the HTML that your server sends, so that it exists beforemain.jsruns. Angular uses the first element that has the attribute.
If your framework creates <mapvx-website> in the browser, put the attribute on another element
that is in the server HTML, for example <div ngCspNonce="NONCE_VALUE" hidden></div>.
The nonce covers every <style> element that Angular injects for the component, and Font
Awesome’s fa-auto-css. It doesn’t cover:
- The Google Analytics loader. The bundle adds that
<script>without a nonce, and there is no option to pass one. Allowhttps://www.googletagmanager.cominscript-src, or use'strict-dynamic'. - Inline
styleattributes. They needstyle-src-attr 'unsafe-inline'. Browsers ignore'unsafe-inline'in a directive that also contains a nonce, which is why attributes get their own directive. styles.css. It’s a regular stylesheet, allowed by'self'or by the CDN’s origin.
If you can’t generate a nonce per response, for example on fully static hosting, use
style-src 'self' 'unsafe-inline' and leave out style-src-attr, which then falls back to
style-src. With neither a nonce nor 'unsafe-inline', the browser blocks the injected <style>
elements and the component renders without its layout styles.
Alternative with 'strict-dynamic'
For a nonce-based script policy, use a nonce and 'strict-dynamic' in script-src, and put the
nonce on the main.js tag. Scripts that the bundle adds itself, including the Google Analytics
loader, are then trusted without listing their hosts. The other directives stay as in the
allowlist policy; this variant also produced no violations.
// Replaces script-src in the allowlist policy, before the header value is built
directives['script-src'] = [`'nonce-${nonce}'`, "'strict-dynamic'"]<script type="module" nonce="NONCE_VALUE" src="/mapvx-website/1.6.1/main.js"></script>- The same nonce value can serve
script-srcandstyle-src. - With
'strict-dynamic', browsers ignore host sources and'self'inscript-src. The nonce on the tag is what allowsmain.js, wherever it’s served from, and a host list can’t keep the Google Analytics loader out in this mode. styles.cssstill needs its origin instyle-srcwhen it comes from a CDN.
Third-party services
These are the hosts that the visitor’s browser contacts because of the component:
| Host | Purpose | When |
|---|---|---|
public-api.mapvx.com | MapVX API: places, configuration, institutions, search, place details and map style | Once api-key is set |
api.mapvx.com | MapVX API: walking directions | When the visitor requests a route |
tiles.mapvx.com, api.maptiler.com and other hosts named by the map style | Map tiles, sprites and label glyphs | While a map is displayed |
Your institution’s image hosts, images.ctfassets.net | Place images, and the placeholder for places without one | When cards and profiles are displayed |
www.googletagmanager.com, *.google-analytics.com, *.analytics.google.com | Google Analytics 4, MapVX’s own property | As soon as main.js loads, even without an api-key |
sentry.mapvx.app | Error reports to MapVX | Only when an error is captured |
www.google.com | Google Maps, opened in a new tab by Driving directions | Only when the visitor clicks that link |
If you load the bundle from unpkg or jsDelivr, the browser also contacts that CDN.
What the MapVX API receives. Your API key (in the Authorization header), the visitor’s
search terms, the IDs of the places they open and, when the visitor picks Use current location
as the starting point of a route, their coordinates, which the API needs to calculate the route.
Like any request from a browser, these requests also carry the visitor’s IP address and standard
browser headers. The component makes no MapVX API request until api-key is set.
The API key is visible. It’s written in your page’s HTML as the api-key attribute, so anyone
can read it.
Google Analytics
The bundle includes Google Analytics 4 and sends events to MapVX’s own GA4 property, on every
page that loads main.js:
- As soon as
main.jsis evaluated, before any<mapvx-website>exists and even without anapi-key, it reuseswindow.dataLayerandwindow.gtagif your page defines them (or creates them), queues MapVX’sconfigcommand with the initial page view turned off, and appends thegtag.jsloader fromhttps://www.googletagmanager.comto<head>, without a nonce. - Each component event is sent with
send_toset to MapVX’s measurement ID, so it goes only to MapVX’s property, not to yours. - There is no attribute or API to turn it off.
What the events contain. Every event carries these parameters:
| Parameter | Content |
|---|---|
host | Your page’s host name (location.hostname). |
sessionId | A random ID generated in the browser. It isn’t stored: a new one is created on each page load and when the element is removed. |
apiKey | The api-key value, which identifies your integration. |
institutionId | The selected institution (from institution-id, or the city the visitor picks). |
parentPlaceId | The building shown on the map, if any. |
The events describe what the visitor does in the component: session start and end (with its
duration), the screens they view (as internal paths such as /list?view=grid or /profile/<id>),
the grid/map toggle, category filters, search terms and result counts, the IDs, names and aliases
of the places they open or select (cards, markers, route origin and destination), the URLs of the
place websites and driving-directions links they click, and clicks on the map and route controls.
The visitor’s coordinates are never included: the location button logs only the click, and a route
from the current location is logged as current-location.
On top of these parameters, gtag.js sends the data that Google Analytics collects by default with
every hit, such as your page’s URL, and sets its first-party cookies (see
Browser storage).
Activity on your page. MapVX’s config command applies to the whole page, not only to the
component, so MapVX’s property also receives:
- the events that your own code sends with
gtag('event', …)withoutsend_to; - the events that Google Analytics measures automatically under the settings of MapVX’s property.
In a test in September 2026, these were scrolling, clicks on links to other sites, page views
when your page changes its URL through the History API, and search terms read from your page’s
query string (such as
?q=).
These automatic events depend on the settings of MapVX’s Google Analytics property, not on the bundle, so they can change without a new version of the component.
Consent. Account for this processing in your privacy notice and consent flow. The component has no switch of its own; the options are:
- Load
main.jsonly after the visitor consents. The Google Analytics setup runs when the bundle is evaluated, so the component can’t render before that point either. - Set Google Consent Mode defaults before
main.jsloads. MapVX’s tag uses your page’sdataLayer, so it follows them. In a test withanalytics_storageset to'denied',gtag.jsset no cookies but still sent hits without them. - Block
https://www.googletagmanager.cominscript-src(allowlist policy).gtag.jsdoesn’t load, and the component keeps working. CSP ignores query strings when it matches sources, so you can’t allow your owngtag.jsfrom that host and block MapVX’s: leaving the host out blocks both. Under'strict-dynamic', a host list can’t block it.
If your page runs its own Google tags, check the result in your tag debugging tools.
Error reporting
In 1.6.1, error reports come from the bundled maps SDK: it sends its own errors, for example a
failed request to the MapVX API, to MapVX’s error tracking at https://sentry.mapvx.app. Errors
raised by the component outside the SDK are only written to the browser console.
- The reporting client is created when the SDK initializes, once
api-keyis set, and it sends a report only when it captures an error. - It’s an isolated Sentry client: it doesn’t call
Sentry.init(), and it leaves out the integrations that hook global state (global error handlers, timer and event-listener wrappers, automatic breadcrumbs forconsole, DOM,fetch, XHR and history, and sessions). It doesn’t touchwindow.onerror,window.onunhandledrejectionor your own Sentry setup. One default integration patchesFunction.prototype.toStringpage-wide; the patch returns the original result for functions that aren’t Sentry’s. - A report carries the error message and stack trace, the full URL of your page (query string
included), the
User-AgentandReferer, the browser’s locale and time zone, the SDK release, and a trail of the SDK calls made before the error, with the IDs of some of the places involved. - There is no attribute to turn it off. Blocking
https://sentry.mapvx.appinconnect-srcstops the reports; the browser then logs a CSP violation for each blocked report.
The scope of error reporting can change between versions: review What’s new before you upgrade.
Browser permissions
The component requests no permission on load. On render it only reads the geolocation permission state, which doesn’t show a prompt.
| Permission | When the browser asks | What it’s used for |
|---|---|---|
| Geolocation | When the visitor clicks the map’s location button, or picks Use current location as the starting point of a route. | Showing and following the visitor’s position; a route’s starting point. |
| Motion and orientation | On browsers that require explicit permission (iOS Safari), when location tracking starts. | Rotating the location marker to the compass heading. |
- If the visitor denies location access, the location button does nothing and Use current location is hidden. The rest of the component keeps working.
- Once tracking is on, the position is watched continuously, with high accuracy, while a map is displayed. Tracking pauses when the map leaves the screen and resumes when a map is shown again.
- Positions stay in the browser, except for a route’s starting point, which is sent to the MapVX API as described in Third-party services.
- Geolocation requires a secure context (HTTPS). If your page is embedded in a cross-origin
<iframe>, the iframe needsallow="geolocation"; a same-origin iframe has access by default. If your site sends aPermissions-Policyheader, it must not disablegeolocation. - The component asks for no other permission (camera, microphone, notifications…).
Browser storage
| Storage | Key | Content | Written by |
|---|---|---|---|
localStorage | mapvx:active-view | grid or map: the visitor’s last Grid View / Map View choice. | The component, when the visitor clicks the toggle. |
| Cookies | _ga and related | Google Analytics identifiers. | gtag.js, as in any Google Analytics installation. |
mapvx:active-viewapplies to the whole origin: every page and every<mapvx-website>on your site shares it, and it overridesdefault-to-mapon later visits (see Remembered view).- If storage is unavailable, for example because the browser blocks site data, the component
ignores the error and falls back to
default-to-map. - The component sets no cookies itself. Neither it nor the bundled SDK stores data in
sessionStorage, IndexedDB or the Cache API, and neither registers a service worker; the SDK’s caches live in memory only. - The analytics session ID is not stored.
If a policy or permission issue shows up as a visible symptom, see Troubleshooting.