Skip to content

Data map

This page lists every field Native Observability writes to its database tables: where the value comes from, whether it can be personal data, and how an MCP tool may expose it. It exists to decide what MCP tools may return. Every claim cites a file:line in the module code; where the code does not settle a question the entry says unverified and why.

Paths are relative to the module root. Line numbers are approximate: they drift as the code changes.

Exposure grades

Decided on 31/10/2026.

Grade What an MCP tool may return
0 Nothing.
1 Aggregates grouped by route name (for example entity.node.canonical), never by path, and only for groups that reach a minimum request count.
2 Row-level detail filtered through PrivacyPolicy, never the IP address. It needs two confirmations: a site-level declaration and the observability:raw scope on the client. A token gets the scope only if the client asks for it at sign-in once the declaration is recorded: tokens issued before keep their scopes, and a refresh never adds it.

In the tables below, Origin is internal when the module or Drupal generates the value, and external when the visitor or a third party writes or influences it (path, query string, user agent, referer, headers, route parameters, SQL values, remote URLs). The Grade 2 column uses these treatments: expose, truncate + mark untrusted, redact, omit.

What the outward exports read from these tables

The Prometheus scrape and the OpenTelemetry exporter read none of the external fields above except as follows. Neither one sends path, payload.path, payload.query, headers, referers, payload.exception.message or the outbound span uri, url, target and error_message.

Consumer Fields used
Prometheus Trace: route_name, method, status_code, duration_ms, is_ajax. Spans: category, duration_ms and the host part of payload.host. Cache events: status, cache_layer.
OpenTelemetry Request: method, route_name, status_code, duration_ms, request id, exception class. uid when capture.store_uid is on. No outbound HTTP span and no SQL statement is exported.

The six tables below are the ones in scope. Two other places hold request data and are listed under Other stores.

native_observability_trace

One row per main request. Schema: native_observability.install:56-150. Writer: src/EventSubscriber/TraceSubscriber.php:324-337 through src/Service/DatabaseTraceStorage.php:84-116.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability.install:59
request_id varchar(64) internal (generated ULID) no omit expose native_observability.install:65; generated at src/EventSubscriber/TraceSubscriber.php:123
method varchar(16) external (request line; the HTTP client picks it) no allowed as a group key only if the route group stays above the threshold; otherwise omit expose native_observability.install:71; src/EventSubscriber/TraceSubscriber.php:326
path varchar(255) external (the visitor chooses it, including for unmatched routes) possible: segments can carry user ids, slugs, search terms, email addresses typed in URLs. Stored redacted: route parameters named like secrets, the core one-time login shapes and token-like segments become [REDACTED] omit never returned: route is the location native_observability.install:77; src/EventSubscriber/TraceSubscriber.php:327
route_name varchar(255), nullable internal (defined by code) when matched; NULL when no route matched no expose as the group key expose native_observability.install:83; src/EventSubscriber/TraceSubscriber.php:328
status_code smallint internal no aggregate (counts per class) expose native_observability.install:89; src/EventSubscriber/TraceSubscriber.php:329
is_ajax tinyint external (derived from an X-Requested-With header, a _wrapper_format query value or a _drupal_ajax body value) no omit expose native_observability.install:96; src/EventSubscriber/TraceSubscriber.php:354-358
duration_ms int internal no aggregate (avg, percentiles) expose native_observability.install:104; src/EventSubscriber/TraceSubscriber.php:252
started_at int (s) internal no bucket to the window expose native_observability.install:110; src/EventSubscriber/TraceSubscriber.php:332
ended_at int (s), nullable internal; always written as NULL by the trace subscriber no omit expose native_observability.install:116; src/EventSubscriber/TraceSubscriber.php:333
uid int, nullable internal yes: a stable user identifier, in an indirect form. Written unless capture.store_uid is off (default on) omit redact (omit, or replace with anonymous/authenticated) native_observability.install:122; src/EventSubscriber/TraceSubscriber.php:287-291; default config/install/native_observability.settings.yml:17
client_ip varchar(64), nullable external (network peer, or X-Forwarded-For when Drupal trusts a proxy; unverified: the trusted-proxy setup is site configuration, outside this module) yes: IP address. Default mode anonymized (last IPv4 octet zeroed, IPv6 truncated to /64). hashed is a salted SHA-256 of the full IP; full stores the raw IP omit omit (grade 2 never returns the IP, whatever the mode) native_observability.install:128; src/EventSubscriber/TraceSubscriber.php:476-491; default config/install/native_observability.settings.yml:20
payload blob (JSON) mixed, see Trace payload keys possible omit expose per key, following the key table native_observability.install:134; src/EventSubscriber/TraceSubscriber.php:257-336

Retention: age-based deletion at trace_retention_hours (default 72) plus a row cap retention.max_rows (default 50000), both run from cron in batches of cleanup_batch_limit. See src/Service/TraceCleanupHandler.php:56-91,104-133, src/Service/ObservabilityCleanupManager.php:40-48,164-175, native_observability.module:16-17, defaults config/install/native_observability.settings.yml:94-103. Cleanup is skipped when cleanup_enabled is off (src/Service/ObservabilityCleanupManager.php:43). Manual full purge: src/Service/TraceCleanupHandler.php:138-155.

Trace payload keys

The JSON in payload is built at src/EventSubscriber/TraceSubscriber.php:257-316. Keys stored:

Key Origin Personal data Grade 2 treatment Source
request_id internal no expose :258
parent_request_id external: read from the request header X-Native-Observability-Parent-Request-Id and stored only when it is a ULID or a UUID of at most 64 characters no, but the client chose the value truncate + mark untrusted :131-134, :259
method external no expose :260
path external possible (see above) never returned :261
query_keys external no: names only, sorted, at most 20; odd or token-like names become [KEY] return as is PrivacyPolicy::queryKeyNames()
is_ajax external-derived no expose :262
route_name internal no expose :265-267 (switch capture.route_name)
status_code internal no expose :269-271
duration_ms internal no expose :273-275
database internal numbers (request summary of the database observer) no expose :277-280; keys built at modules/native_observability_database_observer/src/Service/DatabaseQueryRuntimeCollector.php:71-74, plus stored_slow_queries, slow_threshold_ms at modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:204-207
exception (class, message, code) class internal; message external or mixed: the text of any thrown exception, which can embed request values, SQL, file paths or user input. Stored with URLs and referers redacted, database arguments and quoted literals removed, cut to capture.exception_message_max_length (default 1024) possible class: expose; message: never returned :165-169, :282-285
user_agent external (request header). Default mode family stores only {browser, os} from a fixed vocabulary; mode full stores the raw header family: no. full: possible (device model, build identifiers) family: expose. full: truncate + mark untrusted :298-301, :502-516, :781; default config/install/native_observability.settings.yml:24
headers external. Off by default. allowlist stores the listed headers, full stores all headers except a hard deny-list possible: Referer, X-Forwarded-For, Accept-Language, custom headers omit unless the key is on a module-side allowlist; truncate + mark untrusted :303-306, :527-562; deny-list :58-70; default config/install/native_observability.settings.yml:55
query external. Off by default. keys_only stores parameter names, allowlist and full store values (sensitive names replaced by [REDACTED], other values cut at 2048 bytes) possible: search terms, email addresses, tracking ids omit; at most keys, truncated, marked untrusted :308-311, :573-610, :701-707, :720-730; default config/install/native_observability.settings.yml:60
body external. Off by default (config calls it dangerous). Same modes and redaction as query, applied to parsed form fields possible: form submissions omit :313-316, :621-662; default config/install/native_observability.settings.yml:65

native_observability_spans

Timeline entries linked to a request. Schema: native_observability.install:152-236 (the table belongs to the base module although the writers live in native_observability_spans; see modules/native_observability_spans/native_observability_spans.install:8). Writers: modules/native_observability_spans/src/Service/SpanRecorder.php:100-117 (start) and :160-170 (end, which overwrites the payload), modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:137-148, modules/native_observability_spans/src/EventSubscriber/DatabaseSummarySpanSubscriber.php:132-143.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability.install:155
trace_id int internal; written as 0 by all three writers no omit expose native_observability.install:161; SpanRecorder.php:102, RequestSummarySpanSubscriber.php:138, DatabaseSummarySpanSubscriber.php:133
span_id varchar(64), unique internal (generated) no omit expose native_observability.install:167
request_id varchar(64) internal no omit expose native_observability.install:173
parent_span_id varchar(64), nullable internal no omit expose native_observability.install:179
name varchar(255) mixed. Internal for http.request_summary and database.request_summary. For outbound HTTP spans: METHOD host, where the host comes from a URL that site code or a visitor-supplied value can influence (external), or a caller-supplied native_observability.name option. For custom spans: whatever the calling code passes possible: a host can embed an identifier (for example a tenant subdomain). Not verified for any real case omit truncate + mark untrusted native_observability.install:185; modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:189-201
category varchar(64) internal. Subtype is restricted to [a-z0-9_.] no allowed as a group key expose native_observability.install:191; GuzzleSpanMiddleware.php:146-154
started_at_ms bigint internal no bucket expose native_observability.install:197
ended_at_ms bigint, nullable internal no omit expose native_observability.install:204
duration_ms int internal no aggregate expose native_observability.install:211
payload blob (JSON) mixed, see below possible omit expose per key, following the key table native_observability.install:217

Retention: spans_retention_hours (default 24) and retention.max_rows (default 50000), with started_at_ms converted to milliseconds. src/Service/SpansCleanupHandler.php:56-95,108-137,145-162; defaults config/install/native_observability.settings.yml:104.

Span payload keys

Three families, by writer.

Outbound HTTP span (GuzzleSpanMiddleware), keys at modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:75-83 and :120-127:

Key Origin Personal data Grade 2 treatment
type, method, host method internal, host external no expose / truncate + mark untrusted
uri, url, target (same value three times) external: the full outbound URL, passed through PrivacyPolicy::redactUri() (userinfo and values of sensitive query parameters replaced). Path segments and non-sensitive query values are kept possible: ids and search terms inside the URL truncate + mark untrusted
options internal: an allow-list of Guzzle request options by name and scalar type (timeouts and a few boolean flags). Everything else is dropped, path and array values are kept as booleans only no omit
status_code external (remote response) no expose
outcome internal no expose
error_message external: the exception message of the HTTP client. The response excerpt that Guzzle appends is cut, the original URI is replaced by its redacted form, and the URLs left in the text are redacted possible truncate + mark untrusted

Request summary span, modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:128-134,170-177: synthetic, summary_type, request.{method, path, route_name, status_code, is_ajax} (path through redactSensitivePath(), external), and any fragment returned by an enrichment provider. The one provider in the family adds cache.{page_cache_status, dynamic_page_cache_status, cacheability_status, tags_count, contexts_count, max_age} (modules/native_observability_cache_observer/src/Service/CacheSpanEnrichmentProvider.php:52-81), all internal. Other modules can register providers (modules/native_observability_spans/src/Service/RequestSummaryEnrichmentProviderInterface.php); their keys are unverified for third-party code.

Database summary span, modules/native_observability_spans/src/EventSubscriber/DatabaseSummarySpanSubscriber.php:125-129: synthetic, summary_type, and database (request summary, internal numbers: context_type, query_count, total_query_time_ms, slow_query_count, ...; DatabaseQueryRuntimeCollector.php:71-74).

Custom spans: any caller of SpanRecorderInterface::startSpan() can pass an arbitrary array that is stored as JSON (modules/native_observability_spans/src/Service/SpanRecorder.php:58,111). The module cannot say what other code puts there: unverified by construction.

native_observability_request_exclusion_rule

Admin-authored rules that keep requests out of capture. Schema: native_observability.install:238-304. Writer: src/Service/RequestLogExclusionRepository.php:129-170, fed by src/Form/RequestLogExclusionRuleForm.php.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability.install:241
label varchar(255) internal (admin text; can be prefilled from a query-string value on the add form) possible: free text by an administrator omit truncate + mark untrusted native_observability.install:246; src/Form/RequestLogExclusionRuleForm.php:65,86-90
enabled tinyint internal no omit expose native_observability.install:251
method varchar(16) internal no omit expose native_observability.install:258
path_match_type varchar(16) internal (exact, prefix, regex) no omit expose native_observability.install:264
path_pattern varchar(255) internal, but typically copied from a real visitor path possible omit truncate + mark untrusted native_observability.install:270
query_conditions blob (JSON) internal, typically copied from real query strings (key|equals|value) possible omit truncate + mark untrusted native_observability.install:275; src/Form/RequestLogExclusionRuleForm.php:64,127-131; format src/Service/RequestLogExclusionRuleNormalizer.php:62
notes text internal (admin free text) possible omit truncate + mark untrusted native_observability.install:280
created_at, updated_at int internal no omit expose native_observability.install:285,290

Retention: none found. No cleanup handler covers this table (the handlers registered at native_observability.services.yml:112-125 and by the submodules cover trace, spans, metrics, cache events and database queries). Rules are configuration-like data and are removed only by RequestLogExclusionRepository::delete() (:173) and on module uninstall (native_observability.install:316).

native_observability_route_metric

Aggregates per route and window, computed from the trace table. Schema: modules/native_observability_metrics/native_observability_metrics.install:44-130. Writer: modules/native_observability_metrics/src/Service/DatabaseMetricsStorage.php:44-71; grouping at modules/native_observability_metrics/src/Service/RouteMetricsAggregator.php:50-90.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability_metrics.install:48
route_key varchar(255) internal: the route name when there is one, otherwise the fixed key unmatched no: the visitor path is never used as a key aggregate by this key aggregate by this key native_observability_metrics.install:53; RouteMetricsAggregator.php
route_name varchar(255), nullable internal; holds the key unmatched when no route matched no expose as the group key expose native_observability_metrics.install:59; DatabaseMetricsStorage.php:56
window_start, window_end int internal no expose expose native_observability_metrics.install:65,71
request_count int internal no expose only when at or above the threshold expose native_observability_metrics.install:77
throughput_per_minute decimal internal no expose expose native_observability_metrics.install:83
avg_duration_ms, p95_duration_ms, p99_duration_ms int internal no expose expose native_observability_metrics.install:90,95,101
created_at int internal no expose expose native_observability_metrics.install:108

The aggregator skips system.css_asset and system.js_asset (RouteMetricsAggregator.php:60). A row with request_count of 1 describes a single request, so a minimum count is what keeps a grade 1 answer from being row-level data. The reader used by the MCP tools (RouteMetricsReader) withholds groups under minimum_request_count. The dashboard and the Prometheus reader do not apply a minimum (unverified for the dashboard, not audited here).

Retention: metrics_retention_days (default 7); the row cap is deliberately not applied. src/Service/MetricsCleanupHandler.php:59-94 (comment at :72-75); default config/install/native_observability.settings.yml:105. The window is replaced on recompute (DatabaseMetricsStorage.php:44-49).

native_observability_cache_event

Cache observations and tag invalidations. Schema: modules/native_observability_cache_observer/native_observability_cache_observer.install:13-110. Writers: modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:70-110,143-155, modules/native_observability_cache_observer/src/StackMiddleware/CachePageObserverMiddleware.php:104-117, modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:132-153.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability_cache_observer.install:17
request_id varchar(64), nullable internal no omit expose :23
event_type varchar(64) internal no allowed as a group key expose :29
cache_layer varchar(64) internal no allowed as a group key expose :35
status varchar(64) internal in practice (HIT, MISS, UNCACHEABLE, INVALIDATED). For the header-based events it is the upper-cased value of a response header that Drupal or another module sets no aggregate expose :41; CacheResponseObserverSubscriber.php:134-143, CachePageObserverMiddleware.php:97-104
method varchar(16) external no omit expose :47
path varchar(255) external possible (same as the trace path) omit truncate + mark untrusted :53; CacheResponseObserverSubscriber.php:75, CachePageObserverMiddleware.php:110, ObservedCacheTagsInvalidator.php:138
route_name varchar(255) internal no expose as the group key expose :59
tag_count, context_count, max_age int internal no aggregate expose :65,72,79
created_at int internal no bucket expose :84
payload blob (JSON) internal values. Keys: tags, contexts, max_age (cacheability events); header_value (header events); tags (invalidation events) possible: cache tags are entity identifiers such as node:123, user:5, config:...; one request is unlikely to expose a person, but a tag names the entity omit expose contexts and max_age; truncate the tag list :90; CacheResponseObserverSubscriber.php:101-105,150-152; ObservedCacheTagsInvalidator.php:150-152

The tag list is cut only by the per-request cap max_stored_invalidations_per_request (default 50) on invalidation rows (ObservedCacheTagsInvalidator.php:124-127), not on cacheability rows.

Retention: retention_hours (default 72) and the shared row cap. modules/native_observability_cache_observer/src/Service/CacheObserverCleanupHandler.php:61-85; default modules/native_observability_cache_observer/config/install/native_observability_cache_observer.settings.yml:6.

native_observability_database_query

Slow database queries. Schema: modules/native_observability_database_observer/native_observability_database_observer.install:13-128. Writer: modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:134-208 through modules/native_observability_database_observer/src/Service/DatabaseQueryInsertBuffer.php:25-45.

Field Type Origin Personal data Grade 1 treatment Grade 2 treatment Source
id serial internal no omit expose native_observability_database_observer.install:17
request_id varchar(64), nullable internal no omit expose :22
trace_id int, nullable internal; always written as NULL no omit expose :28; DatabaseQueryObserverSubscriber.php:163
route_name varchar(255), nullable internal no expose as the group key expose :34; :164
method varchar(16) external no omit expose :40; :165
path varchar(255) external possible (same as the trace path) omit truncate + mark untrusted :46; :141,166
relation_type varchar(16) internal no allowed as a group key expose :52
connection_key, connection_target varchar internal no omit expose :59,65
duration_ms decimal internal no aggregate expose :71
is_slow tinyint internal no omit expose :79
query_hash varchar(64) internal: SHA-256 of the SQL text, unsalted no. A hash of a statement that contains literals is guessable for short inputs; unverified whether real statements do allowed as a group key expose :87; modules/native_observability_database_observer/src/Service/DatabaseQueryClassifier.php:38-40
query_text blob, nullable mixed: the SQL string with placeholders, as Drupal logs it. Written unless store_slow_query_text is off (default off) possible. See SQL text omit truncate + mark untrusted, or omit :93; DatabaseQueryObserverSubscriber.php:144,160,173; default modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:3
caller varchar(255), nullable internal (Class::method from the call stack) no omit expose :99; DatabaseQueryObserverSubscriber.php:177,235-252
created_at int internal no bucket expose :105
payload blob (JSON) internal. Keys: context_type, time_seconds, request_summary (the request summary numbers plus slow_threshold_ms) no omit expose :111; DatabaseQueryObserverSubscriber.php:193-199

Only queries at or above slow_query_threshold_ms (default 100) are stored, at most max_stored_queries_per_request per request (default 20), keeping the slowest. DatabaseQueryObserverSubscriber.php:143-145,156-158,183; defaults modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:2-4.

Retention: retention_hours (default 72) and the shared row cap. modules/native_observability_database_observer/src/Service/DatabaseObserverCleanupHandler.php:58-86; default modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:5.

SQL text and query arguments

  • Query arguments are not stored. The observer reads the query entry of each log record and ignores args (DatabaseQueryObserverSubscriber.php:150-179; the caller array also carries args and formatCaller() uses only class and function, :244-251). Core's log record keeps the SQL string and the bound values apart (web/core/lib/Drupal/Core/Database/Log.php:114-120 in the Drupal 11 tree used on the test bench).
  • The SQL text is not stored by default (store_slow_query_text: false). When a site turns it on, the text goes through SqlLiteralRedactor before the insert: string and numeric literals become ? and IN lists collapse to IN (?). The statement hash is taken from the redacted text. Identifiers built from input stay in the text.
  • The SQL text is not passed through PrivacyPolicy. The redactor is the only step between Database::getLog() and the insert (DatabaseQueryObserverSubscriber.php).
  • The form calls the flag "Store SQL text for slow queries" and describes it only as a storage-size choice (modules/native_observability_database_observer/src/Form/DatabaseObserverSettingsForm.php:85-90).

Other stores

These are not in scope as tables but hold request data.

  • Private tempstore, collection native_observability_execution: the execution timeline of authenticated users, up to 1000 spans, written at response time (modules/native_observability_execution/src/EventSubscriber/ExecutionRequestSubscriber.php:38-49,148-168). It holds the redacted path and, for outbound HTTP, the URL passed through PrivacyPolicy::redactUri() (modules/native_observability_execution/src/Service/GuzzleTraceMiddleware.php). Where tempstore keeps it and for how long is core behavior, unverified here.
  • State: native_observability.client_ip_hash_salt, or $settings['native_observability_client_ip_hash_salt']; never in config.
  • Config: the sensitive-key list (config/install/native_observability.settings.yml:21,31-48).

Externally-controlled fields

Every field below is written or influenced by a visitor or an external party. Each one is a prompt-injection test case: an MCP tool that returns it hands attacker-chosen text to the model.

# Table Field Who controls it Source
1 native_observability_trace path the visitor (any URL, including unmatched ones) src/EventSubscriber/TraceSubscriber.php:327
2 native_observability_trace payload.path the visitor src/EventSubscriber/TraceSubscriber.php:261
3 native_observability_trace payload.parent_request_id the visitor (request header, stored only when it is a ULID or a UUID) src/EventSubscriber/TraceSubscriber.php:131-134,259
4 native_observability_trace payload.exception.message whoever triggers the exception; may embed request values src/EventSubscriber/TraceSubscriber.php:165-169
5 native_observability_trace payload.user_agent (mode full; mode family is a fixed vocabulary) the visitor (request header) src/EventSubscriber/TraceSubscriber.php:502-516
6 native_observability_trace payload.headers.* (when headers_mode is not off) the visitor (request headers, including Referer) src/EventSubscriber/TraceSubscriber.php:527-562
7 native_observability_trace payload.query.* (when query_mode is not off; keys in keys_only) the visitor src/EventSubscriber/TraceSubscriber.php:573-610
8 native_observability_trace payload.body.* (when body_mode is not off) the visitor src/EventSubscriber/TraceSubscriber.php:621-662
9 native_observability_trace client_ip (mode full; also a fixed-shape value in other modes) the network peer or a forwarding proxy src/EventSubscriber/TraceSubscriber.php:476-491
10 native_observability_spans name (outbound HTTP spans: METHOD host; custom spans: any string) the owner of the remote URL; the caller for custom names modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:189-201
11 native_observability_spans payload.uri, payload.url, payload.target the remote URL (path and query values survive redaction) modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:75-81
12 native_observability_spans payload.error_message the remote server and the network stack modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:115-126
13 native_observability_spans payload.status_code the remote server (integer) modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:123
14 native_observability_spans payload.request.path (request summary span) the visitor modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:173
15 native_observability_spans payload of custom spans any code calling startSpan() / endSpan() modules/native_observability_spans/src/Service/SpanRecorder.php:58,111,160-170
16 native_observability_cache_event path the visitor modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:75
17 native_observability_cache_event payload.tags (cache tags carry entity ids; some modules build tags from input) code that sets tags; unverified whether visitor input can shape a tag modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:150-152
18 native_observability_cache_event status / payload.header_value (response header value) the code that sets X-Drupal-Cache / X-Drupal-Dynamic-Cache; unverified whether a third party can set it modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:130-143
19 native_observability_database_query path the visitor modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:141,166
20 native_observability_database_query query_text (literal values written into SQL, when a statement has any) the visitor, through code that puts input directly into SQL; unverified for any real statement modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:160,173
21 native_observability_database_query method (and method in trace and cache tables) the client (request method; arbitrary token accepted by Symfony) modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:140,165; src/EventSubscriber/TraceSubscriber.php:326
22 native_observability_request_exclusion_rule label, path_pattern, query_conditions, notes an administrator; values are typically copied from visitor URLs src/Service/RequestLogExclusionRepository.php:139-145

PrivacyPolicy coverage

Class: src/Service/PrivacyPolicy.php, registered as native_observability.privacy_policy (native_observability.services.yml:73-75).

What it covers

Capability Method Behavior Source
Sensitive path segments redactSensitivePath() Replaces the one-time credentials in three core URL shapes (/user/reset/{uid}/{ts}/{hash}[/login], /user/{uid}/cancel/confirm/{ts}/{hash}) with [REDACTED], with or without language prefix and trailing slash. Route parameters named like secrets and token-like segments are redacted too. The uid stays :232-247
Sensitive parameter names isSensitiveKey(), isSensitiveName(), sensitiveKeys() Case-insensitive match on the whole name or one segment (key matches access_key, not keyword). Default list of 29 names; replaced by capture.sensitive_keys; switch capture.redact_sensitive_keys :31-49,76-91,142-162
URIs redactUri(), redactSensitiveUri() Values of sensitive query parameters and the password in the userinfo become [REDACTED]; the path is not inspected :99-102,284-331
Client IP applyToIp(), anonymize(), hash() Modes off, anonymized, hashed, full from capture.client_ip_mode :59-66,171-214

Where it is applied

Call Writer Source
redactSensitivePath() trace row and payload src/EventSubscriber/TraceSubscriber.php:261,327
redactSensitivePath() request summary span modules/native_observability_spans/src/EventSubscriber/RequestSummarySpanSubscriber.php:173
redactSensitivePath() cache events (three writers) modules/native_observability_cache_observer/src/EventSubscriber/CacheResponseObserverSubscriber.php:75; modules/native_observability_cache_observer/src/StackMiddleware/CachePageObserverMiddleware.php:110; modules/native_observability_cache_observer/src/Service/ObservedCacheTagsInvalidator.php:138
redactSensitivePath() database query rows modules/native_observability_database_observer/src/EventSubscriber/DatabaseQueryObserverSubscriber.php:141
redactSensitivePath() execution timeline (tempstore) modules/native_observability_execution/src/EventSubscriber/ExecutionRequestSubscriber.php:127
isSensitiveName() query and body values in the trace payload src/EventSubscriber/TraceSubscriber.php:701-707
redactUri() outbound HTTP span URI, error message, host fallback modules/native_observability_spans/src/Http/GuzzleSpanMiddleware.php:73,117,193
sensitiveKeys() default list settings form src/Form/NativeObservabilitySettingsForm.php:339

The IP mode is applied through PrivacyPolicy::applyToIp(), called by the trace subscriber. IPv4 loses the last octet and IPv6 keeps the first 64 bits.

The trace subscriber also keeps its own deny-list of headers that are never stored (authorization, cookie, set-cookie, x-csrf-token, x-drupal-csrf-token, x-forwarded-authorization, and the calibration token header): src/EventSubscriber/TraceSubscriber.php:58-70. It is separate from PrivacyPolicy.

What is not covered

  • Untrusted-text marking. PrivacyPolicy does not mark a stored string as visitor-controlled. The query classes do it on read, through src/Query/UntrustedText.php.
  • Path segments that carry identifiers or text. Only credential shapes, sensitive route parameters and token-like segments are matched. /user/5, /search/jane.doe@example.com or /node/123/edit are stored as sent (:233-237).
  • Query values in the path-like fields. path is getPathInfo(), which has no query string, so the query is not in the path columns. The query is stored only in payload.query when query_mode is not off, where only sensitive names are redacted and the other values are kept (cut at 2048 bytes); names that are not in the list are not (src/EventSubscriber/TraceSubscriber.php:594,604,701-707).
  • Exception messages. Redacted by PrivacyPolicy::redactExceptionMessage() and cut to a maximum length. Free text in them that is not a URL is stored as it is. The tools never return the message.
  • SQL text. Literals are redacted, identifiers are not (SqlLiteralRedactor.php).
  • Header values other than the deny-list: the sensitive-key list applies to query and body names, not to header names (src/EventSubscriber/TraceSubscriber.php:527-562).
  • uid. Stored in its column unless capture.store_uid is off. PrivacyPolicy::storesUid() reports the switch to every sink.
  • Reading side. PrivacyPolicy is applied when writing. Nothing in the policy filters rows on read, so an MCP tool must apply its own grade filter. Whether the dashboard, report and export readers add filtering is unverified (not audited for this page); the report's privacy-aware export controls are mentioned in docs/index.md.

Findings that may require core changes

Facts only. Each item names a value stored, or a gap, that matters for the exposure grades.

  1. Unmatched requests share one key. Requests without a route are stored under the fixed route_key unmatched, so the metrics table holds no visitor path. modules/native_observability_metrics/src/Service/RouteMetricsAggregator.php.
  2. parent_request_id comes from a request header. It is stored only when it is a ULID or a UUID of at most 64 characters (src/EventSubscriber/TraceSubscriber.php). The client still chose it, so tools return it as untrusted text.
  3. Exception messages are stored redacted and are never returned. They are cut to capture.exception_message_max_length. src/EventSubscriber/TraceSubscriber.php:165-169,282-285. Setting: capture.exception, default on (config/install/native_observability.settings.yml:14).
  4. SQL text is optional and redacted. store_slow_query_text defaults to false (modules/native_observability_database_observer/config/install/native_observability_database_observer.settings.yml:3). Arguments are never stored. Literals in the SQL string are replaced by SqlLiteralRedactor.
  5. IP hashing is salted. The salt is generated on first use, kept in State (PrivacyPolicy::SALT_STATE_KEY) and never written to config; $settings['native_observability_client_ip_hash_salt'] overrides it.
  6. Small counts are written. request_count of 1 is stored (RouteMetricsAggregator.php). The reader used by the tools withholds groups under minimum_request_count (RouteMetricsReader.php).
  7. No retention for the exclusion rule table. See above; the free-text fields label and notes have no cleanup path other than deleting the rule.
  8. Any code can write arbitrary JSON into native_observability_spans.payload. modules/native_observability_spans/src/Service/SpanRecorder.php:58,111,160-170. The family cannot vouch for what third-party callers store.