| updated | 2026-06-25 |
|---|---|
| commit | 31a127e |
Gatherstead uses Azure Monitor OpenTelemetry (Azure.Monitor.OpenTelemetry.AspNetCore) with a two-stage PII guard: a PiiRedactionLogProcessor (logs) and a PiiRedactionActivityProcessor (spans) that run before every export. This document is the authoritative reference for what you are allowed to log and why.
Log entity IDs, never entity fields.
If you need to correlate a log entry to a record in the database, log its Guid primary key. Never log a name, email address, phone number, birth date, dietary note, address, or any other value sourced from a HouseholdMember, ContactMethod, Address, DietaryProfile, or similar PII-bearing entity.
Bad:
_logger.LogWarning("Cannot edit member {MemberName}", member.FirstName);Good:
_logger.LogWarning("Member edit denied for {MemberId}", member.Id);The processors maintain an explicit allowlist. Any structured log property or span tag whose key is not on this list is replaced with [redacted] before export. When redaction occurs, LogRecord.FormattedMessage is also cleared so the expanded string doesn't leak values.
| Key (case-insensitive) | Safe value |
|---|---|
TenantId / tenant_id / tenant.id |
Internal Guid |
UserId / user_id / user.id |
Internal Guid |
MemberId / member_id / member.id |
Internal Guid |
HouseholdId / household_id / household.id |
Internal Guid |
EventId / event_id / event.id |
Internal Guid |
AccommodationId / accommodation_id / accommodation.id |
Internal Guid |
CorrelationId / correlation_id / correlation.id |
OTel TraceId string |
EntityTenantId, CurrentTenantId |
Internal Guid (cross-tenant log) |
| Key | Safe value |
|---|---|
Jti / jti |
JWT token ID (not the token itself) |
Role / UserRole / TenantRole / HouseholdRole |
Enum name |
RequiredRole |
Enum name |
Reason |
Short string (revocation reason, authz denial reason) |
EventType |
Enum name |
Severity |
Enum name |
EntityType |
.NET type name |
Count, Method, Path |
Numeric / HTTP verb / URL path |
| Key | Safe value |
|---|---|
{OriginalFormat} |
Message template string (no values) |
CategoryName |
Logger category name |
All http.*, net.*, rpc.*, service.*, code.*, otel.*, messaging.*, exception.* attributes are allowed. Within db.*, only db.system, db.name, db.operation, and db.sql.table are allowed — db.statement and db.query.text are always redacted by the Activity processor regardless of allowlist membership, because SQL text can contain bound parameter values.
If you need to log a new safe attribute (e.g., a new aggregate ID), add it to the AllowedKeys set in PiiRedactionLogProcessor.cs and update this document in the same PR. The allowlist is shared between the log and span processors via PiiRedactionLogProcessor.IsAllowed().
When reviewing any PR that touches logging, verify:
- Every structured log parameter is either an ID, enum, count, or short metadata string.
- No entity field values (
Name,Email,BirthDate,Phone,Notes,DietaryRestrictions, etc.) appear as log arguments. - New attribute keys are either in the existing allowlist or have been explicitly added with a justification comment.
- Exception messages caught and logged do not originate from user-controlled input that could embed PII.
The Nuxt frontend uses the Application Insights JavaScript SDK
(@microsoft/applicationinsights-web), initialized in
app/plugins/analytics.client.ts
and consumed through the useAnalytics()
composable. It is client-only and no-ops when no connection string is configured
(local dev).
Conventions:
- Cookieless.
disableCookiesUsage: true— no consent banner, no persistent identifiers. - Separate destinations. Demo → its own App Insights component (
appi-gat-demo-*); Prod → the sharedappi-gat-*(same resource as the backend, giving end-to-end frontend↔backend trace correlation). Every item is tagged withai.cloud.role(gatherstead-web/gatherstead-web-demo). - Session/user stitching (hybrid). Demo and anonymous Prod pages group a visit by an
in-memory session GUID (resets on full reload). Authenticated Prod users are stitched via
setAuthenticatedUserContext(userId, tenantId, /*storeInCookie*/ false)— no cookie. - The one rule still applies. Custom event names, property values, and the
setUserarguments must be opaque IDs / enums / counts / short metadata only — never member names, emails, notes, birth dates, or any other entity field value. - Two instrumentation layers.
- Within-SPA non-persisted interactions (modal opens, view toggles, the demo→live CTA)
are tracked in components/composables via
useAnalytics().trackEvent(...)and fire in both modes — they never reach the backend, so the frontend is the only place they exist. - Persisted actions (create/update/delete) are tracked at the repository layer via
trackPersistence(entity, action, props?)inapp/utils/telemetry.ts— a module-level accessor because repos run outside Vue/Nuxt context. They fire in both modes: in Demo they are the only record (no backend); in Prod they intentionally complement the backend API trace with frontend-session funnel correlation. Event names match across Demo and Live repos (e.g.event_create,attendance_set) so the two destinations align. Extend coverage by callingtrackPersistencefrom additional repo write methods.
- Within-SPA non-persisted interactions (modal opens, view toggles, the demo→live CTA)
are tracked in components/composables via
- Demographics are PII-safe. Coarse geo (city/region/country) is derived at ingestion and
the IP is not stored; browser/OS come from the User-Agent;
language/localeare sent as custom dimensions. None identify an individual.
The connection string is delivered as NUXT_PUBLIC_APP_INSIGHTS_CONNECTION_STRING — a web-app
app setting in Prod (read at runtime) and a GitHub Actions secret baked into the static Demo
build at pnpm generate time. The underscore placement matters: Nuxt binds it to
runtimeConfig.public.appInsightsConnectionString only because Nitro derives the env name as
snakeCase('appInsightsConnectionString').toUpperCase() → APP_INSIGHTS_CONNECTION_STRING. It is
an ingestion-only key and safe to expose in client code.
The allowlist is validated by unit tests in PiiRedactionLogProcessorTests and PiiRedactionActivityProcessorTests. Run dotnet test to confirm all assertions pass after any allowlist change.