@valentia-ai-skills/framework 2.0.7 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -6
- package/bin/cli.js +772 -56
- package/package.json +1 -1
- package/skills/global/aisupportapp-project-architecture/SKILL.md +1 -1
- package/skills/global/aisupportapp-project-conventions/SKILL.md +1 -1
- package/skills/global/aisupportapp-project-workflows/SKILL.md +1 -1
- package/skills/global/api-design/SKILL.md +1 -1
- package/skills/global/appointment-oas-app/SKILL.md +1 -1
- package/skills/global/code-quality-auditor/SKILL.md +704 -0
- package/skills/global/code-standards/SKILL.md +1 -1
- package/skills/global/codebase-legacy-intelligence/SKILL.md +1 -1
- package/skills/global/legacy-api-converter/SKILL.md +979 -0
- package/skills/global/legacy-redevelopment-planner/SKILL.md +622 -0
- package/skills/global/observability-integrations/SKILL.md +835 -0
- package/skills/global/project-scanner/SKILL.md +1 -1
- package/skills/global/ui-replication-engine/SKILL.md +591 -0
- package/skills/global/aisupportapp-test-installation/SKILL.md +0 -32
- package/skills/global/viteapp-core-workflows/SKILL.md +0 -32
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observability-integrations
|
|
3
|
+
description: Integrates observability, analytics, and monitoring services into React applications — Sentry
|
|
4
|
+
(error tracking + performance), Microsoft Clarity (heatmaps + session recording), Datadog
|
|
5
|
+
(RUM + logs + APM), Google Analytics/Pixels (GA4 + GTM + conversion tracking), and Syslog
|
|
6
|
+
(structured logging for SIEM systems). Each service has its own detailed integration guide
|
|
7
|
+
with healthcare-grade PII protection, consent management, and environment-based feature flags.
|
|
8
|
+
Use this skill whenever someone asks to: add error tracking, integrate Sentry, add analytics,
|
|
9
|
+
set up Clarity, add Datadog monitoring, integrate Google Analytics, add GA4, set up conversion
|
|
10
|
+
pixels, add session recording, configure syslog, send logs to SIEM, add observability, set up
|
|
11
|
+
monitoring, track errors in production, add heatmaps, or integrate any tracking/analytics
|
|
12
|
+
service. Also trigger when someone says things like "I need error tracking", "add Sentry to
|
|
13
|
+
my React app", "set up analytics", "we need session recordings", "integrate Datadog RUM",
|
|
14
|
+
"add Google Tag Manager", "send logs to our SIEM", "we need observability", "track user
|
|
15
|
+
behavior", "monitor performance", or "add crash reporting". Works with any React application
|
|
16
|
+
— Vite, Next.js, Create React App, or custom webpack setups.
|
|
17
|
+
version: 1.0.0
|
|
18
|
+
scope: global
|
|
19
|
+
last_reviewed: 2026-04-02
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
name: observability-integrations
|
|
24
|
+
description: >
|
|
25
|
+
Integrates observability, analytics, and monitoring services into React applications — Sentry
|
|
26
|
+
(error tracking + performance), Microsoft Clarity (heatmaps + session recording), Datadog
|
|
27
|
+
(RUM + logs + APM), Google Analytics/Pixels (GA4 + GTM + conversion tracking), and Syslog
|
|
28
|
+
(structured logging for SIEM systems). Each service has its own detailed integration guide
|
|
29
|
+
with healthcare-grade PII protection, consent management, and environment-based feature flags.
|
|
30
|
+
Use this skill whenever someone asks to: add error tracking, integrate Sentry, add analytics,
|
|
31
|
+
set up Clarity, add Datadog monitoring, integrate Google Analytics, add GA4, set up conversion
|
|
32
|
+
pixels, add session recording, configure syslog, send logs to SIEM, add observability, set up
|
|
33
|
+
monitoring, track errors in production, add heatmaps, or integrate any tracking/analytics
|
|
34
|
+
service. Also trigger when someone says things like "I need error tracking", "add Sentry to
|
|
35
|
+
my React app", "set up analytics", "we need session recordings", "integrate Datadog RUM",
|
|
36
|
+
"add Google Tag Manager", "send logs to our SIEM", "we need observability", "track user
|
|
37
|
+
behavior", "monitor performance", or "add crash reporting". Works with any React application
|
|
38
|
+
— Vite, Next.js, Create React App, or custom webpack setups.
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
# Observability Integrations
|
|
42
|
+
|
|
43
|
+
You are a senior frontend platform engineer specializing in observability, monitoring, and analytics integration. You add tracking services to React applications with **healthcare-grade privacy** — every integration automatically scrubs PII, respects user consent, and operates behind environment-based feature flags with instant kill switches.
|
|
44
|
+
|
|
45
|
+
## Core Principles — Apply to EVERY Integration
|
|
46
|
+
|
|
47
|
+
1. **Privacy first**: In healthcare applications, patient data (NHI, names, DOB, medical records) MUST NEVER reach any third-party tracking service. Scrub automatically, don't rely on developers remembering.
|
|
48
|
+
|
|
49
|
+
2. **Consent before tracking**: No tracking service initializes until the user has consented. Provide a consent management layer that all services hook into.
|
|
50
|
+
|
|
51
|
+
3. **Environment isolation**: Tracking behaves differently per environment — disabled in development, sampled in staging, full in production. Never pollute production analytics with dev/test data.
|
|
52
|
+
|
|
53
|
+
4. **Kill switch**: Every service can be disabled instantly via environment variable without code changes or redeployment.
|
|
54
|
+
|
|
55
|
+
5. **Performance budget**: Each tracking script adds load time. Monitor total impact. Warn if combined tracking scripts exceed 100KB gzipped.
|
|
56
|
+
|
|
57
|
+
6. **Structured, not scattered**: All integrations go through a centralized observability layer — not scattered `window.gtag()` calls throughout the codebase.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Step 0: Understand the Application
|
|
62
|
+
|
|
63
|
+
### Check for Legacy Intelligence
|
|
64
|
+
|
|
65
|
+
If legacy project documentation exists (`.ai-skills/legacy-projects/{project}/` or `./{project}-intelligence/`):
|
|
66
|
+
|
|
67
|
+
1. Read `MASTER_SKILL.md` — understand the application architecture, module map
|
|
68
|
+
2. Read `BUSINESS_RULES.md` — identify which user actions are business-critical (these become tracked events)
|
|
69
|
+
3. Read `API_REGISTRY.md` — identify which API calls should be monitored for performance
|
|
70
|
+
4. Read `DATA_MODELS.md` — identify which fields contain PII/PHI that must be scrubbed from ALL tracking
|
|
71
|
+
|
|
72
|
+
### Ask the User
|
|
73
|
+
|
|
74
|
+
1. **Which service(s)?** Present the options:
|
|
75
|
+
- Sentry (error tracking, performance monitoring, session replay)
|
|
76
|
+
- Microsoft Clarity (heatmaps, session recording, behavior analytics)
|
|
77
|
+
- Datadog (RUM, logs, APM, full-stack monitoring)
|
|
78
|
+
- Google Analytics / Pixels (GA4, GTM, conversion tracking)
|
|
79
|
+
- Syslog (structured logging → SIEM forwarding)
|
|
80
|
+
|
|
81
|
+
2. **React setup?** Vite, Next.js, CRA, or custom?
|
|
82
|
+
|
|
83
|
+
3. **Is this a healthcare/medical application?** If yes, apply maximum PII scrubbing rules automatically.
|
|
84
|
+
|
|
85
|
+
4. **Does the app handle payments?** If yes, configure e-commerce/conversion event tracking for GA4.
|
|
86
|
+
|
|
87
|
+
5. **Existing tracking?** Are there any tracking scripts already in the app that need to coexist?
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Step 1: Build the Observability Foundation
|
|
92
|
+
|
|
93
|
+
Before integrating any specific service, build the shared infrastructure that ALL services use.
|
|
94
|
+
|
|
95
|
+
### 1.1 Folder Structure
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
src/
|
|
99
|
+
├── observability/
|
|
100
|
+
│ ├── index.ts ← Public API — re-exports everything
|
|
101
|
+
│ ├── config.ts ← Environment-based configuration
|
|
102
|
+
│ ├── consent.ts ← Consent management
|
|
103
|
+
│ ├── pii-scrubber.ts ← PII/PHI scrubbing engine
|
|
104
|
+
│ ├── performance-budget.ts ← Script size monitoring
|
|
105
|
+
│ ├── types.ts ← Shared types
|
|
106
|
+
│ ├── providers/
|
|
107
|
+
│ │ ├── ObservabilityProvider.tsx ← React context provider wrapping all services
|
|
108
|
+
│ │ └── ConsentBanner.tsx ← GDPR/privacy consent UI
|
|
109
|
+
│ ├── hooks/
|
|
110
|
+
│ │ ├── useTrackEvent.ts ← Unified event tracking hook
|
|
111
|
+
│ │ ├── useTrackPageView.ts ← SPA page view tracking
|
|
112
|
+
│ │ ├── useConsent.ts ← Consent state hook
|
|
113
|
+
│ │ └── useErrorBoundary.ts ← Error boundary hook
|
|
114
|
+
│ └── services/
|
|
115
|
+
│ ├── sentry.ts ← Sentry service adapter
|
|
116
|
+
│ ├── clarity.ts ← Clarity service adapter
|
|
117
|
+
│ ├── datadog.ts ← Datadog service adapter
|
|
118
|
+
│ ├── google-analytics.ts ← GA4 service adapter
|
|
119
|
+
│ └── syslog.ts ← Syslog service adapter
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 1.2 Configuration
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// src/observability/config.ts
|
|
126
|
+
|
|
127
|
+
export interface ObservabilityConfig {
|
|
128
|
+
environment: 'development' | 'staging' | 'production'
|
|
129
|
+
isHealthcareApp: boolean
|
|
130
|
+
|
|
131
|
+
sentry: {
|
|
132
|
+
enabled: boolean
|
|
133
|
+
dsn: string
|
|
134
|
+
tracesSampleRate: number // 0-1
|
|
135
|
+
replaySampleRate: number // 0-1
|
|
136
|
+
replayOnErrorSampleRate: number
|
|
137
|
+
release?: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
clarity: {
|
|
141
|
+
enabled: boolean
|
|
142
|
+
projectId: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
datadog: {
|
|
146
|
+
enabled: boolean
|
|
147
|
+
applicationId: string
|
|
148
|
+
clientToken: string
|
|
149
|
+
site: string // e.g., 'datadoghq.com'
|
|
150
|
+
service: string
|
|
151
|
+
version?: string
|
|
152
|
+
sampleRate: number
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
googleAnalytics: {
|
|
156
|
+
enabled: boolean
|
|
157
|
+
measurementId: string // G-XXXXXXXXXX
|
|
158
|
+
gtmContainerId?: string // GTM-XXXXXXX
|
|
159
|
+
enableConversions: boolean
|
|
160
|
+
debugMode: boolean
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
syslog: {
|
|
164
|
+
enabled: boolean
|
|
165
|
+
endpoint: string // Backend log collector URL
|
|
166
|
+
level: 'debug' | 'info' | 'warn' | 'error'
|
|
167
|
+
batchSize: number // Flush after N logs
|
|
168
|
+
flushInterval: number // Flush every N ms
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
consent: {
|
|
172
|
+
required: boolean // If true, nothing loads until consent given
|
|
173
|
+
storageKey: string // localStorage key for consent state
|
|
174
|
+
categories: ('necessary' | 'analytics' | 'marketing' | 'performance')[]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
piiFields: string[] // Field names to always scrub
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function loadConfig(): ObservabilityConfig {
|
|
181
|
+
const env = import.meta.env.MODE || process.env.NODE_ENV || 'development'
|
|
182
|
+
const isProd = env === 'production'
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
environment: env as any,
|
|
186
|
+
isHealthcareApp: import.meta.env.VITE_HEALTHCARE_APP === 'true',
|
|
187
|
+
|
|
188
|
+
sentry: {
|
|
189
|
+
enabled: import.meta.env.VITE_SENTRY_ENABLED === 'true',
|
|
190
|
+
dsn: import.meta.env.VITE_SENTRY_DSN || '',
|
|
191
|
+
tracesSampleRate: isProd ? 0.2 : 1.0,
|
|
192
|
+
replaySampleRate: isProd ? 0.1 : 0,
|
|
193
|
+
replayOnErrorSampleRate: 1.0,
|
|
194
|
+
release: import.meta.env.VITE_APP_VERSION,
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
clarity: {
|
|
198
|
+
enabled: import.meta.env.VITE_CLARITY_ENABLED === 'true',
|
|
199
|
+
projectId: import.meta.env.VITE_CLARITY_PROJECT_ID || '',
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
datadog: {
|
|
203
|
+
enabled: import.meta.env.VITE_DATADOG_ENABLED === 'true',
|
|
204
|
+
applicationId: import.meta.env.VITE_DATADOG_APP_ID || '',
|
|
205
|
+
clientToken: import.meta.env.VITE_DATADOG_CLIENT_TOKEN || '',
|
|
206
|
+
site: import.meta.env.VITE_DATADOG_SITE || 'datadoghq.com',
|
|
207
|
+
service: import.meta.env.VITE_DATADOG_SERVICE || '',
|
|
208
|
+
version: import.meta.env.VITE_APP_VERSION,
|
|
209
|
+
sampleRate: isProd ? 100 : 0,
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
googleAnalytics: {
|
|
213
|
+
enabled: import.meta.env.VITE_GA_ENABLED === 'true',
|
|
214
|
+
measurementId: import.meta.env.VITE_GA_MEASUREMENT_ID || '',
|
|
215
|
+
gtmContainerId: import.meta.env.VITE_GTM_CONTAINER_ID,
|
|
216
|
+
enableConversions: import.meta.env.VITE_GA_CONVERSIONS === 'true',
|
|
217
|
+
debugMode: !isProd,
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
syslog: {
|
|
221
|
+
enabled: import.meta.env.VITE_SYSLOG_ENABLED === 'true',
|
|
222
|
+
endpoint: import.meta.env.VITE_SYSLOG_ENDPOINT || '',
|
|
223
|
+
level: (import.meta.env.VITE_SYSLOG_LEVEL || 'info') as any,
|
|
224
|
+
batchSize: 10,
|
|
225
|
+
flushInterval: 5000,
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
consent: {
|
|
229
|
+
required: true, // Default: require consent
|
|
230
|
+
storageKey: 'observability_consent',
|
|
231
|
+
categories: ['necessary', 'analytics', 'performance'],
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// Healthcare PII fields — ALWAYS scrub from ALL services
|
|
235
|
+
piiFields: [
|
|
236
|
+
'nhi', 'NHI', 'nhi_number', 'nhiNumber',
|
|
237
|
+
'firstName', 'first_name', 'lastName', 'last_name',
|
|
238
|
+
'fullName', 'full_name', 'name',
|
|
239
|
+
'dateOfBirth', 'date_of_birth', 'dob', 'DOB',
|
|
240
|
+
'email', 'phone', 'mobile', 'address',
|
|
241
|
+
'ssn', 'socialSecurityNumber',
|
|
242
|
+
'medicalRecordNumber', 'mrn',
|
|
243
|
+
'password', 'token', 'apiKey', 'secret',
|
|
244
|
+
'creditCard', 'cardNumber', 'cvv', 'expiryDate',
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 1.3 PII Scrubber
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// src/observability/pii-scrubber.ts
|
|
254
|
+
|
|
255
|
+
const DEFAULT_REPLACEMENT = '[REDACTED]'
|
|
256
|
+
|
|
257
|
+
export class PiiScrubber {
|
|
258
|
+
private sensitiveFields: Set<string>
|
|
259
|
+
private sensitivePatterns: RegExp[]
|
|
260
|
+
|
|
261
|
+
constructor(fields: string[]) {
|
|
262
|
+
this.sensitiveFields = new Set(fields.map(f => f.toLowerCase()))
|
|
263
|
+
|
|
264
|
+
// Patterns that catch PII even in unstructured text
|
|
265
|
+
this.sensitivePatterns = [
|
|
266
|
+
/[A-Z]{3}\d{4}/g, // NZ NHI number
|
|
267
|
+
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, // Phone numbers
|
|
268
|
+
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, // Email addresses
|
|
269
|
+
/\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, // Credit card numbers
|
|
270
|
+
/\b\d{2}[-/]\d{2}[-/]\d{4}\b/g, // Date of birth patterns
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Scrub an object recursively
|
|
275
|
+
scrubObject<T extends Record<string, any>>(obj: T): T {
|
|
276
|
+
if (!obj || typeof obj !== 'object') return obj
|
|
277
|
+
|
|
278
|
+
const scrubbed = Array.isArray(obj) ? [...obj] : { ...obj }
|
|
279
|
+
|
|
280
|
+
for (const key of Object.keys(scrubbed)) {
|
|
281
|
+
if (this.sensitiveFields.has(key.toLowerCase())) {
|
|
282
|
+
(scrubbed as any)[key] = DEFAULT_REPLACEMENT
|
|
283
|
+
} else if (typeof scrubbed[key] === 'object' && scrubbed[key] !== null) {
|
|
284
|
+
(scrubbed as any)[key] = this.scrubObject(scrubbed[key])
|
|
285
|
+
} else if (typeof scrubbed[key] === 'string') {
|
|
286
|
+
(scrubbed as any)[key] = this.scrubString(scrubbed[key])
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return scrubbed as T
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Scrub PII patterns from a string
|
|
294
|
+
scrubString(str: string): string {
|
|
295
|
+
let result = str
|
|
296
|
+
for (const pattern of this.sensitivePatterns) {
|
|
297
|
+
result = result.replace(pattern, DEFAULT_REPLACEMENT)
|
|
298
|
+
}
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Scrub URL query parameters
|
|
303
|
+
scrubUrl(url: string): string {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = new URL(url, 'http://localhost')
|
|
306
|
+
for (const [key] of parsed.searchParams) {
|
|
307
|
+
if (this.sensitiveFields.has(key.toLowerCase())) {
|
|
308
|
+
parsed.searchParams.set(key, DEFAULT_REPLACEMENT)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return parsed.pathname + parsed.search
|
|
312
|
+
} catch {
|
|
313
|
+
return url
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 1.4 Consent Management
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// src/observability/consent.ts
|
|
323
|
+
|
|
324
|
+
export type ConsentCategory = 'necessary' | 'analytics' | 'marketing' | 'performance'
|
|
325
|
+
|
|
326
|
+
export interface ConsentState {
|
|
327
|
+
given: boolean
|
|
328
|
+
timestamp: string | null
|
|
329
|
+
categories: Record<ConsentCategory, boolean>
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const DEFAULT_STATE: ConsentState = {
|
|
333
|
+
given: false,
|
|
334
|
+
timestamp: null,
|
|
335
|
+
categories: {
|
|
336
|
+
necessary: true, // Always allowed
|
|
337
|
+
analytics: false,
|
|
338
|
+
marketing: false,
|
|
339
|
+
performance: false,
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export class ConsentManager {
|
|
344
|
+
private state: ConsentState
|
|
345
|
+
private storageKey: string
|
|
346
|
+
private listeners: ((state: ConsentState) => void)[] = []
|
|
347
|
+
|
|
348
|
+
constructor(storageKey: string) {
|
|
349
|
+
this.storageKey = storageKey
|
|
350
|
+
this.state = this.load()
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private load(): ConsentState {
|
|
354
|
+
try {
|
|
355
|
+
const stored = localStorage.getItem(this.storageKey)
|
|
356
|
+
return stored ? JSON.parse(stored) : { ...DEFAULT_STATE }
|
|
357
|
+
} catch {
|
|
358
|
+
return { ...DEFAULT_STATE }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private save(): void {
|
|
363
|
+
try {
|
|
364
|
+
localStorage.setItem(this.storageKey, JSON.stringify(this.state))
|
|
365
|
+
} catch { /* localStorage unavailable */ }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
grantAll(): void {
|
|
369
|
+
this.state = {
|
|
370
|
+
given: true,
|
|
371
|
+
timestamp: new Date().toISOString(),
|
|
372
|
+
categories: { necessary: true, analytics: true, marketing: true, performance: true },
|
|
373
|
+
}
|
|
374
|
+
this.save()
|
|
375
|
+
this.notify()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
grantSelected(categories: Partial<Record<ConsentCategory, boolean>>): void {
|
|
379
|
+
this.state = {
|
|
380
|
+
given: true,
|
|
381
|
+
timestamp: new Date().toISOString(),
|
|
382
|
+
categories: { ...this.state.categories, ...categories, necessary: true },
|
|
383
|
+
}
|
|
384
|
+
this.save()
|
|
385
|
+
this.notify()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
revoke(): void {
|
|
389
|
+
this.state = { ...DEFAULT_STATE }
|
|
390
|
+
this.save()
|
|
391
|
+
this.notify()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
hasConsent(category: ConsentCategory): boolean {
|
|
395
|
+
return this.state.categories[category] === true
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
isConsentGiven(): boolean {
|
|
399
|
+
return this.state.given
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getState(): ConsentState {
|
|
403
|
+
return { ...this.state }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
onChange(listener: (state: ConsentState) => void): () => void {
|
|
407
|
+
this.listeners.push(listener)
|
|
408
|
+
return () => { this.listeners = this.listeners.filter(l => l !== listener) }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private notify(): void {
|
|
412
|
+
this.listeners.forEach(l => l(this.getState()))
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Service → consent category mapping
|
|
417
|
+
export const SERVICE_CONSENT_MAP: Record<string, ConsentCategory> = {
|
|
418
|
+
sentry: 'necessary', // Error tracking is necessary for app stability
|
|
419
|
+
clarity: 'analytics', // Session recording requires analytics consent
|
|
420
|
+
datadog: 'performance', // Performance monitoring
|
|
421
|
+
googleAnalytics: 'analytics',
|
|
422
|
+
syslog: 'necessary', // Operational logging is necessary
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### 1.5 Unified Tracking Hook
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// src/observability/hooks/useTrackEvent.ts
|
|
430
|
+
|
|
431
|
+
import { useCallback } from 'react'
|
|
432
|
+
import { useObservability } from '../providers/ObservabilityProvider'
|
|
433
|
+
|
|
434
|
+
export interface TrackEventOptions {
|
|
435
|
+
name: string
|
|
436
|
+
category?: string
|
|
437
|
+
properties?: Record<string, any>
|
|
438
|
+
services?: ('sentry' | 'clarity' | 'datadog' | 'ga' | 'syslog')[] // default: all enabled
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function useTrackEvent() {
|
|
442
|
+
const { services, scrubber } = useObservability()
|
|
443
|
+
|
|
444
|
+
return useCallback((options: TrackEventOptions) => {
|
|
445
|
+
const scrubbedProps = options.properties
|
|
446
|
+
? scrubber.scrubObject(options.properties)
|
|
447
|
+
: undefined
|
|
448
|
+
|
|
449
|
+
const targetServices = options.services || Object.keys(services)
|
|
450
|
+
|
|
451
|
+
for (const svc of targetServices) {
|
|
452
|
+
if (services[svc]?.isEnabled()) {
|
|
453
|
+
services[svc].trackEvent(options.name, options.category, scrubbedProps)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}, [services, scrubber])
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 1.6 SPA Page View Tracking
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
// src/observability/hooks/useTrackPageView.ts
|
|
464
|
+
|
|
465
|
+
import { useEffect } from 'react'
|
|
466
|
+
import { useLocation } from 'react-router-dom'
|
|
467
|
+
import { useObservability } from '../providers/ObservabilityProvider'
|
|
468
|
+
|
|
469
|
+
export function useTrackPageView() {
|
|
470
|
+
const location = useLocation()
|
|
471
|
+
const { services, scrubber } = useObservability()
|
|
472
|
+
|
|
473
|
+
useEffect(() => {
|
|
474
|
+
const scrubbedPath = scrubber.scrubUrl(location.pathname + location.search)
|
|
475
|
+
|
|
476
|
+
for (const svc of Object.values(services)) {
|
|
477
|
+
if (svc.isEnabled()) {
|
|
478
|
+
svc.trackPageView(scrubbedPath, document.title)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}, [location.pathname])
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### 1.7 ObservabilityProvider
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
// src/observability/providers/ObservabilityProvider.tsx
|
|
489
|
+
|
|
490
|
+
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
491
|
+
import { loadConfig, ObservabilityConfig } from '../config'
|
|
492
|
+
import { ConsentManager, SERVICE_CONSENT_MAP } from '../consent'
|
|
493
|
+
import { PiiScrubber } from '../pii-scrubber'
|
|
494
|
+
|
|
495
|
+
// Each service adapter implements this interface
|
|
496
|
+
export interface ObservabilityService {
|
|
497
|
+
name: string
|
|
498
|
+
init(): void
|
|
499
|
+
shutdown(): void
|
|
500
|
+
isEnabled(): boolean
|
|
501
|
+
trackEvent(name: string, category?: string, properties?: Record<string, any>): void
|
|
502
|
+
trackPageView(path: string, title: string): void
|
|
503
|
+
setUser(userId: string, traits?: Record<string, any>): void
|
|
504
|
+
trackError(error: Error, context?: Record<string, any>): void
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
interface ObservabilityContextValue {
|
|
508
|
+
services: Record<string, ObservabilityService>
|
|
509
|
+
config: ObservabilityConfig
|
|
510
|
+
consent: ConsentManager
|
|
511
|
+
scrubber: PiiScrubber
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const ObservabilityContext = createContext<ObservabilityContextValue | null>(null)
|
|
515
|
+
|
|
516
|
+
export function useObservability() {
|
|
517
|
+
const ctx = useContext(ObservabilityContext)
|
|
518
|
+
if (!ctx) throw new Error('useObservability must be used within ObservabilityProvider')
|
|
519
|
+
return ctx
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function ObservabilityProvider({ children }: { children: React.ReactNode }) {
|
|
523
|
+
const config = useMemo(() => loadConfig(), [])
|
|
524
|
+
const consent = useMemo(() => new ConsentManager(config.consent.storageKey), [])
|
|
525
|
+
const scrubber = useMemo(() => new PiiScrubber(config.piiFields), [])
|
|
526
|
+
const [services, setServices] = useState<Record<string, ObservabilityService>>({})
|
|
527
|
+
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
// Initialize services that have consent
|
|
530
|
+
const initServices: Record<string, ObservabilityService> = {}
|
|
531
|
+
|
|
532
|
+
// Import and init each enabled service
|
|
533
|
+
// Read the relevant reference file for each service's adapter implementation
|
|
534
|
+
// See: references/sentry.md, references/clarity.md, etc.
|
|
535
|
+
|
|
536
|
+
const handleConsentChange = () => {
|
|
537
|
+
// Re-evaluate which services should be active
|
|
538
|
+
for (const [name, svc] of Object.entries(initServices)) {
|
|
539
|
+
const category = SERVICE_CONSENT_MAP[name]
|
|
540
|
+
if (category && !consent.hasConsent(category)) {
|
|
541
|
+
svc.shutdown()
|
|
542
|
+
} else if (category && consent.hasConsent(category) && !svc.isEnabled()) {
|
|
543
|
+
svc.init()
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
consent.onChange(handleConsentChange)
|
|
549
|
+
setServices(initServices)
|
|
550
|
+
|
|
551
|
+
return () => {
|
|
552
|
+
Object.values(initServices).forEach(svc => svc.shutdown())
|
|
553
|
+
}
|
|
554
|
+
}, [])
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<ObservabilityContext.Provider value={{ services, config, consent, scrubber }}>
|
|
558
|
+
{children}
|
|
559
|
+
</ObservabilityContext.Provider>
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### 1.8 Environment Variables Template
|
|
565
|
+
|
|
566
|
+
Generate `.env.example`:
|
|
567
|
+
|
|
568
|
+
```bash
|
|
569
|
+
# ── Observability Configuration ──
|
|
570
|
+
|
|
571
|
+
# Healthcare mode — enables maximum PII scrubbing
|
|
572
|
+
VITE_HEALTHCARE_APP=true
|
|
573
|
+
|
|
574
|
+
# App version — used for release tracking
|
|
575
|
+
VITE_APP_VERSION=1.0.0
|
|
576
|
+
|
|
577
|
+
# ── Sentry ──
|
|
578
|
+
VITE_SENTRY_ENABLED=false
|
|
579
|
+
VITE_SENTRY_DSN=
|
|
580
|
+
|
|
581
|
+
# ── Microsoft Clarity ──
|
|
582
|
+
VITE_CLARITY_ENABLED=false
|
|
583
|
+
VITE_CLARITY_PROJECT_ID=
|
|
584
|
+
|
|
585
|
+
# ── Datadog ──
|
|
586
|
+
VITE_DATADOG_ENABLED=false
|
|
587
|
+
VITE_DATADOG_APP_ID=
|
|
588
|
+
VITE_DATADOG_CLIENT_TOKEN=
|
|
589
|
+
VITE_DATADOG_SITE=datadoghq.com
|
|
590
|
+
VITE_DATADOG_SERVICE=
|
|
591
|
+
|
|
592
|
+
# ── Google Analytics ──
|
|
593
|
+
VITE_GA_ENABLED=false
|
|
594
|
+
VITE_GA_MEASUREMENT_ID=
|
|
595
|
+
VITE_GTM_CONTAINER_ID=
|
|
596
|
+
VITE_GA_CONVERSIONS=false
|
|
597
|
+
|
|
598
|
+
# ── Syslog ──
|
|
599
|
+
VITE_SYSLOG_ENABLED=false
|
|
600
|
+
VITE_SYSLOG_ENDPOINT=
|
|
601
|
+
VITE_SYSLOG_LEVEL=info
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## Step 2: Integrate the Selected Service(s)
|
|
607
|
+
|
|
608
|
+
After building the foundation, read the appropriate reference file for each service the user selected:
|
|
609
|
+
|
|
610
|
+
| Service | Reference File | Consent Category |
|
|
611
|
+
|---------|---------------|-----------------|
|
|
612
|
+
| Sentry | `references/sentry.md` | necessary |
|
|
613
|
+
| Microsoft Clarity | `references/clarity.md` | analytics |
|
|
614
|
+
| Datadog | `references/datadog.md` | performance |
|
|
615
|
+
| Google Analytics / Pixels | `references/google-analytics.md` | analytics |
|
|
616
|
+
| Syslog → SIEM | `references/syslog.md` | necessary |
|
|
617
|
+
|
|
618
|
+
Read the selected reference file(s) and implement the service adapter following the patterns defined there.
|
|
619
|
+
|
|
620
|
+
Each reference file contains:
|
|
621
|
+
1. NPM packages to install
|
|
622
|
+
2. The service adapter class implementing `ObservabilityService` interface
|
|
623
|
+
3. Initialization code (where and how to init)
|
|
624
|
+
4. Event tracking patterns specific to that service
|
|
625
|
+
5. PII scrubbing rules specific to that service
|
|
626
|
+
6. Testing/verification steps
|
|
627
|
+
7. Healthcare-specific configuration
|
|
628
|
+
|
|
629
|
+
---
|
|
630
|
+
|
|
631
|
+
## Step 3: Multi-Service Coexistence
|
|
632
|
+
|
|
633
|
+
When integrating multiple services simultaneously:
|
|
634
|
+
|
|
635
|
+
### Load Order
|
|
636
|
+
Services must initialize in this order to avoid conflicts:
|
|
637
|
+
1. **Sentry** first — catches errors from other service init
|
|
638
|
+
2. **Syslog** second — operational logging is always needed
|
|
639
|
+
3. **Datadog** third — performance monitoring before analytics
|
|
640
|
+
4. **Google Analytics** fourth — analytics tracking
|
|
641
|
+
5. **Clarity** last — session recording after everything else is stable
|
|
642
|
+
|
|
643
|
+
### Performance Budget
|
|
644
|
+
Monitor combined script sizes:
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// src/observability/performance-budget.ts
|
|
648
|
+
|
|
649
|
+
const SCRIPT_SIZES_KB = {
|
|
650
|
+
sentry: 72, // @sentry/react + @sentry/browser
|
|
651
|
+
clarity: 45, // Clarity script (loaded async)
|
|
652
|
+
datadog: 55, // @datadog/browser-rum
|
|
653
|
+
ga: 30, // gtag.js
|
|
654
|
+
syslog: 5, // Custom, minimal
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function checkPerformanceBudget(enabledServices: string[]): {
|
|
658
|
+
totalKb: number
|
|
659
|
+
overBudget: boolean
|
|
660
|
+
warning: string | null
|
|
661
|
+
} {
|
|
662
|
+
const totalKb = enabledServices.reduce((sum, svc) => sum + (SCRIPT_SIZES_KB[svc] || 0), 0)
|
|
663
|
+
const overBudget = totalKb > 100
|
|
664
|
+
|
|
665
|
+
return {
|
|
666
|
+
totalKb,
|
|
667
|
+
overBudget,
|
|
668
|
+
warning: overBudget
|
|
669
|
+
? `Tracking scripts total ${totalKb}KB gzipped (budget: 100KB). Consider disabling non-essential services or lazy-loading them.`
|
|
670
|
+
: null,
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Deduplication
|
|
676
|
+
If both Sentry and Datadog are enabled, errors will be captured twice. The adapters should coordinate:
|
|
677
|
+
- Sentry handles detailed error tracking with stack traces
|
|
678
|
+
- Datadog tracks the error as a RUM event without the full stack trace
|
|
679
|
+
- Both get the same `correlationId` for cross-referencing
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
683
|
+
## Step 4: Wiring into the React App
|
|
684
|
+
|
|
685
|
+
### App Entry Point
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
// src/main.tsx or src/App.tsx
|
|
689
|
+
|
|
690
|
+
import { ObservabilityProvider } from './observability/providers/ObservabilityProvider'
|
|
691
|
+
import { ConsentBanner } from './observability/providers/ConsentBanner'
|
|
692
|
+
|
|
693
|
+
function App() {
|
|
694
|
+
return (
|
|
695
|
+
<ObservabilityProvider>
|
|
696
|
+
<ConsentBanner />
|
|
697
|
+
<RouterProvider router={router} />
|
|
698
|
+
</ObservabilityProvider>
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### In Route Components — Page View Tracking
|
|
704
|
+
|
|
705
|
+
```tsx
|
|
706
|
+
// src/layouts/AppLayout.tsx
|
|
707
|
+
|
|
708
|
+
import { useTrackPageView } from '../observability/hooks/useTrackPageView'
|
|
709
|
+
|
|
710
|
+
function AppLayout() {
|
|
711
|
+
useTrackPageView() // Tracks every route change across ALL enabled services
|
|
712
|
+
|
|
713
|
+
return (
|
|
714
|
+
<div>
|
|
715
|
+
<Outlet />
|
|
716
|
+
</div>
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### In Components — Event Tracking
|
|
722
|
+
|
|
723
|
+
```tsx
|
|
724
|
+
// src/features/booking/BookingConfirmation.tsx
|
|
725
|
+
|
|
726
|
+
import { useTrackEvent } from '../../observability/hooks/useTrackEvent'
|
|
727
|
+
|
|
728
|
+
function BookingConfirmation() {
|
|
729
|
+
const trackEvent = useTrackEvent()
|
|
730
|
+
|
|
731
|
+
const handleConfirm = async () => {
|
|
732
|
+
await submitBooking()
|
|
733
|
+
|
|
734
|
+
// Tracked in ALL enabled services automatically
|
|
735
|
+
// PII is scrubbed automatically by the hook
|
|
736
|
+
trackEvent({
|
|
737
|
+
name: 'booking_confirmed',
|
|
738
|
+
category: 'conversion',
|
|
739
|
+
properties: {
|
|
740
|
+
appointmentType: booking.type,
|
|
741
|
+
providerId: booking.providerId,
|
|
742
|
+
// patientName would be auto-scrubbed if included
|
|
743
|
+
},
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Error Boundaries
|
|
750
|
+
|
|
751
|
+
```tsx
|
|
752
|
+
// src/observability/hooks/useErrorBoundary.ts
|
|
753
|
+
|
|
754
|
+
import * as Sentry from '@sentry/react'
|
|
755
|
+
|
|
756
|
+
// Use Sentry's error boundary for automatic error tracking
|
|
757
|
+
export const ErrorBoundary = Sentry.ErrorBoundary
|
|
758
|
+
|
|
759
|
+
// Fallback component
|
|
760
|
+
export function ErrorFallback({ error, resetError }: { error: Error; resetError: () => void }) {
|
|
761
|
+
return (
|
|
762
|
+
<div className="flex flex-col items-center justify-center min-h-screen p-8">
|
|
763
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-4">Something went wrong</h1>
|
|
764
|
+
<p className="text-gray-600 mb-6">We've been notified and are looking into it.</p>
|
|
765
|
+
<Button onClick={resetError}>Try Again</Button>
|
|
766
|
+
</div>
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## Step 5: Verification
|
|
774
|
+
|
|
775
|
+
After integrating, verify each service:
|
|
776
|
+
|
|
777
|
+
### Per-Service Verification Checklist
|
|
778
|
+
|
|
779
|
+
- [ ] Service initializes only when enabled via env var
|
|
780
|
+
- [ ] Service does NOT initialize until consent is given (for analytics/marketing categories)
|
|
781
|
+
- [ ] Service tracks page views on route changes
|
|
782
|
+
- [ ] Service tracks custom events
|
|
783
|
+
- [ ] PII is scrubbed from all tracked data (test with a fake NHI number — it should appear as [REDACTED] in the service dashboard)
|
|
784
|
+
- [ ] Service respects kill switch (set env var to false, confirm no requests to service)
|
|
785
|
+
- [ ] Service does not error when disabled (graceful no-op)
|
|
786
|
+
- [ ] Source maps uploaded (Sentry/Datadog — for readable stack traces)
|
|
787
|
+
- [ ] No patient names, NHI numbers, or medical data appear in ANY service dashboard
|
|
788
|
+
|
|
789
|
+
### Integration Test
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
describe('Observability', () => {
|
|
793
|
+
it('should scrub PII from tracked events', () => {
|
|
794
|
+
const scrubber = new PiiScrubber(config.piiFields)
|
|
795
|
+
const event = {
|
|
796
|
+
name: 'patient_viewed',
|
|
797
|
+
patientName: 'John Smith',
|
|
798
|
+
nhi: 'ABC1234',
|
|
799
|
+
email: 'john@example.com',
|
|
800
|
+
appointmentType: 'General Checkup',
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const scrubbed = scrubber.scrubObject(event)
|
|
804
|
+
|
|
805
|
+
expect(scrubbed.patientName).toBe('[REDACTED]')
|
|
806
|
+
expect(scrubbed.nhi).toBe('[REDACTED]')
|
|
807
|
+
expect(scrubbed.email).toBe('[REDACTED]')
|
|
808
|
+
expect(scrubbed.appointmentType).toBe('General Checkup') // NOT PII, kept
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('should not initialize tracking without consent', () => {
|
|
812
|
+
const consent = new ConsentManager('test_consent')
|
|
813
|
+
expect(consent.hasConsent('analytics')).toBe(false)
|
|
814
|
+
// Clarity and GA should NOT be initialized
|
|
815
|
+
})
|
|
816
|
+
})
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
---
|
|
820
|
+
|
|
821
|
+
## Quality Gate
|
|
822
|
+
|
|
823
|
+
Before delivering:
|
|
824
|
+
|
|
825
|
+
- [ ] Foundation built: config, consent, PII scrubber, provider, hooks
|
|
826
|
+
- [ ] Selected service(s) integrated via adapter pattern
|
|
827
|
+
- [ ] All PII fields from config are scrubbed in ALL services
|
|
828
|
+
- [ ] Consent management working — services only init after consent
|
|
829
|
+
- [ ] Kill switch working — env var disables each service independently
|
|
830
|
+
- [ ] SPA page views tracked on route changes
|
|
831
|
+
- [ ] Error boundary in place with fallback UI
|
|
832
|
+
- [ ] `.env.example` generated with all required variables
|
|
833
|
+
- [ ] No raw `window.gtag()` or `window.clarity()` calls scattered in components — all through the observability layer
|
|
834
|
+
- [ ] Performance budget checked — combined scripts under 100KB warning threshold
|
|
835
|
+
- [ ] Tests verify PII scrubbing and consent behavior
|