@buivietphi/skill-mobile-mt 1.0.0

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.

Potentially problematic release.


This version of @buivietphi/skill-mobile-mt might be problematic. Click here for more details.

@@ -0,0 +1,688 @@
1
+ # Mobile Observability — Sessions as the Fourth Pillar
2
+
3
+ > Sessions unify metrics, logs, and traces into a coherent user journey.
4
+
5
+ ## The Four Pillars of Mobile Observability
6
+
7
+ ```
8
+ Traditional: Metrics + Logs + Traces
9
+ Mobile: Metrics + Logs + Traces + Sessions ← NEW
10
+
11
+ Sessions = The thread that ties everything together
12
+ ```
13
+
14
+ Without sessions, you have isolated signals.
15
+ With sessions, you have a complete user story.
16
+
17
+ ---
18
+
19
+ ## Session Model
20
+
21
+ ### Session Lifecycle
22
+
23
+ ```typescript
24
+ interface MobileSession {
25
+ // Identity
26
+ session_id: string; // Unique ID for this session
27
+ user_id?: string; // Authenticated user (nullable)
28
+ device_id: string; // Stable device identifier
29
+
30
+ // Timing
31
+ started_at: number; // Unix timestamp (ms)
32
+ ended_at?: number; // Null if still active
33
+ duration_ms?: number; // Calculated on end
34
+
35
+ // Mobile Context
36
+ app_version: string; // "2.1.3"
37
+ build_number: string; // "42"
38
+ platform: 'ios' | 'android';
39
+ os_version: string; // "17.2", "14"
40
+ device_model: string; // "iPhone 15 Pro", "Pixel 8"
41
+
42
+ // Network
43
+ network_type: 'wifi' | '5g' | '4g' | '3g' | 'offline';
44
+ carrier?: string;
45
+
46
+ // State
47
+ foreground_time_ms: number; // Time app was visible
48
+ background_time_ms: number; // Time app was in background
49
+ crash_count: number; // Crashes in this session
50
+
51
+ // Correlation
52
+ previous_session_id?: string; // Chain sessions
53
+ acquisition_channel?: string; // How user arrived (deeplink, push, organic)
54
+ }
55
+ ```
56
+
57
+ ### Session Events
58
+
59
+ ```typescript
60
+ type SessionEvent =
61
+ | 'session_start'
62
+ | 'session_end'
63
+ | 'session_pause' // App backgrounded
64
+ | 'session_resume' // App foregrounded
65
+ | 'session_crash' // Crash detected
66
+ | 'session_timeout'; // Inactive for N minutes
67
+
68
+ // Session starts when app becomes active
69
+ // Session ends when app is killed or inactive > 30 minutes
70
+ // Background time > 30 minutes = new session on foreground
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Implementation Pattern
76
+
77
+ ### React Native / Expo
78
+
79
+ ```typescript
80
+ // src/observability/session.ts
81
+ import { AppState, AppStateStatus } from 'react-native';
82
+ import AsyncStorage from '@react-native-async-storage/async-storage';
83
+ import { nanoid } from 'nanoid/non-secure';
84
+ import DeviceInfo from 'react-native-device-info';
85
+ import NetInfo from '@react-native-community/netinfo';
86
+
87
+ class SessionManager {
88
+ private currentSession: MobileSession | null = null;
89
+ private appStateSubscription: any = null;
90
+ private backgroundTimer: NodeJS.Timeout | null = null;
91
+ private readonly SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
92
+
93
+ async startSession(): Promise<MobileSession> {
94
+ const previousSessionId = await this.getPreviousSessionId();
95
+ const netInfo = await NetInfo.fetch();
96
+
97
+ this.currentSession = {
98
+ session_id: nanoid(),
99
+ device_id: await DeviceInfo.getUniqueId(),
100
+ app_version: DeviceInfo.getVersion(),
101
+ build_number: DeviceInfo.getBuildNumber(),
102
+ platform: Platform.OS as 'ios' | 'android',
103
+ os_version: DeviceInfo.getSystemVersion(),
104
+ device_model: DeviceInfo.getModel(),
105
+ network_type: this.mapNetworkType(netInfo.type),
106
+ started_at: Date.now(),
107
+ foreground_time_ms: 0,
108
+ background_time_ms: 0,
109
+ crash_count: 0,
110
+ previous_session_id: previousSessionId,
111
+ };
112
+
113
+ await this.saveSession(this.currentSession);
114
+ this.trackEvent('session_start', this.currentSession);
115
+ this.observeAppState();
116
+
117
+ return this.currentSession;
118
+ }
119
+
120
+ private observeAppState() {
121
+ let lastActiveTime = Date.now();
122
+
123
+ this.appStateSubscription = AppState.addEventListener(
124
+ 'change',
125
+ async (nextState: AppStateStatus) => {
126
+ if (nextState === 'background' || nextState === 'inactive') {
127
+ // App going to background
128
+ this.currentSession!.foreground_time_ms += Date.now() - lastActiveTime;
129
+ this.trackEvent('session_pause', { session_id: this.currentSession!.session_id });
130
+
131
+ // Set timeout for new session on resume
132
+ this.backgroundTimer = setTimeout(() => {
133
+ this.markSessionExpired();
134
+ }, this.SESSION_TIMEOUT_MS);
135
+
136
+ } else if (nextState === 'active') {
137
+ // App returning to foreground
138
+ if (this.backgroundTimer) clearTimeout(this.backgroundTimer);
139
+
140
+ if (this.isSessionExpired()) {
141
+ await this.endSession('session_timeout');
142
+ await this.startSession();
143
+ } else {
144
+ const backgroundStart = lastActiveTime;
145
+ lastActiveTime = Date.now();
146
+ this.currentSession!.background_time_ms += lastActiveTime - backgroundStart;
147
+ this.trackEvent('session_resume', { session_id: this.currentSession!.session_id });
148
+ }
149
+ }
150
+ }
151
+ );
152
+ }
153
+
154
+ async endSession(reason: SessionEvent = 'session_end') {
155
+ if (!this.currentSession) return;
156
+
157
+ this.currentSession.ended_at = Date.now();
158
+ this.currentSession.duration_ms =
159
+ this.currentSession.ended_at - this.currentSession.started_at;
160
+
161
+ this.trackEvent(reason, this.currentSession);
162
+ await this.savePreviousSessionId(this.currentSession.session_id);
163
+ this.currentSession = null;
164
+ }
165
+
166
+ getContext(): Record<string, string> {
167
+ if (!this.currentSession) return {};
168
+ return {
169
+ session_id: this.currentSession.session_id,
170
+ device_id: this.currentSession.device_id,
171
+ app_version: this.currentSession.app_version,
172
+ platform: this.currentSession.platform,
173
+ };
174
+ }
175
+
176
+ // Inject session context into every log/metric/trace
177
+ enrichEvent(event: Record<string, any>): Record<string, any> {
178
+ return {
179
+ ...event,
180
+ ...this.getContext(),
181
+ timestamp: Date.now(),
182
+ };
183
+ }
184
+ }
185
+
186
+ export const sessionManager = new SessionManager();
187
+ ```
188
+
189
+ ### Flutter (Dart)
190
+
191
+ ```dart
192
+ // lib/observability/session_manager.dart
193
+ import 'dart:io';
194
+ import 'package:device_info_plus/device_info_plus.dart';
195
+ import 'package:connectivity_plus/connectivity_plus.dart';
196
+ import 'package:shared_preferences/shared_preferences.dart';
197
+ import 'package:uuid/uuid.dart';
198
+
199
+ class SessionManager {
200
+ static final SessionManager _instance = SessionManager._internal();
201
+ factory SessionManager() => _instance;
202
+ SessionManager._internal();
203
+
204
+ MobileSession? _currentSession;
205
+ final _uuid = const Uuid();
206
+
207
+ Future<MobileSession> startSession() async {
208
+ final prefs = await SharedPreferences.getInstance();
209
+ final previousId = prefs.getString('last_session_id');
210
+ final deviceInfo = DeviceInfoPlugin();
211
+ final connectivity = await Connectivity().checkConnectivity();
212
+
213
+ String deviceId = '';
214
+ String osVersion = '';
215
+ String deviceModel = '';
216
+
217
+ if (Platform.isIOS) {
218
+ final iosInfo = await deviceInfo.iosInfo;
219
+ deviceId = iosInfo.identifierForVendor ?? '';
220
+ osVersion = iosInfo.systemVersion;
221
+ deviceModel = iosInfo.model;
222
+ } else if (Platform.isAndroid) {
223
+ final androidInfo = await deviceInfo.androidInfo;
224
+ deviceId = androidInfo.id;
225
+ osVersion = androidInfo.version.release;
226
+ deviceModel = androidInfo.model;
227
+ }
228
+
229
+ _currentSession = MobileSession(
230
+ sessionId: _uuid.v4(),
231
+ deviceId: deviceId,
232
+ appVersion: '1.0.0', // From package_info_plus
233
+ buildNumber: '1',
234
+ platform: Platform.isIOS ? 'ios' : 'android',
235
+ osVersion: osVersion,
236
+ deviceModel: deviceModel,
237
+ networkType: _mapConnectivity(connectivity),
238
+ startedAt: DateTime.now().millisecondsSinceEpoch,
239
+ previousSessionId: previousId,
240
+ );
241
+
242
+ _trackEvent('session_start', _currentSession!.toMap());
243
+ return _currentSession!;
244
+ }
245
+
246
+ Map<String, String> getContext() {
247
+ final session = _currentSession;
248
+ if (session == null) return {};
249
+ return {
250
+ 'session_id': session.sessionId,
251
+ 'device_id': session.deviceId,
252
+ 'app_version': session.appVersion,
253
+ 'platform': session.platform,
254
+ };
255
+ }
256
+
257
+ // Call this before every log, metric, or trace
258
+ Map<String, dynamic> enrichEvent(Map<String, dynamic> event) {
259
+ return {
260
+ ...event,
261
+ ...getContext(),
262
+ 'timestamp': DateTime.now().millisecondsSinceEpoch,
263
+ };
264
+ }
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Unified Observability Stack
271
+
272
+ ### Signal Unification
273
+
274
+ ```typescript
275
+ // Every signal (metric/log/trace) MUST include session context
276
+
277
+ // ❌ WRONG: Missing session context
278
+ logger.info('User logged in');
279
+ metrics.increment('login_success');
280
+ tracer.startSpan('auth.login');
281
+
282
+ // ✅ CORRECT: Session context injected
283
+ const context = sessionManager.getContext();
284
+
285
+ logger.info('User logged in', { ...context, method: 'email' });
286
+ metrics.increment('login_success', { ...context });
287
+ tracer.startSpan('auth.login', { attributes: context });
288
+ ```
289
+
290
+ ### Correlation Architecture
291
+
292
+ ```
293
+ User Journey (Session Layer)
294
+
295
+ ├── Screen: LoginScreen (session_id: abc123)
296
+ │ ├── Metric: screen_view {session_id: abc123, screen: "login"}
297
+ │ ├── Log: "Attempting login" {session_id: abc123}
298
+ │ └── Trace: auth.login {session_id: abc123}
299
+ │ ├── span: validate_email {duration: 2ms}
300
+ │ ├── span: api.call {duration: 234ms}
301
+ │ └── span: save_token {duration: 5ms}
302
+
303
+ └── Screen: HomeScreen (session_id: abc123)
304
+ ├── Metric: screen_view {session_id: abc123, screen: "home"}
305
+ ├── Log: "Feed loaded" {session_id: abc123, items: 20}
306
+ └── Trace: feed.load {session_id: abc123}
307
+
308
+ QUERY POWER: "Show me all logs, metrics, and traces for session abc123"
309
+ → Complete user journey reconstruction
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Instrumentation Patterns
315
+
316
+ ### Screen Tracking
317
+
318
+ ```typescript
319
+ // ✅ Context-rich screen tracking
320
+ function trackScreen(screenName: string, params?: Record<string, string>) {
321
+ const event = sessionManager.enrichEvent({
322
+ event: 'screen_view',
323
+ screen_name: screenName,
324
+ screen_class: screenName,
325
+ // Avoid PII — see anti-patterns.md
326
+ ...params,
327
+ });
328
+
329
+ analytics.track(event);
330
+ logger.debug('Screen viewed', event);
331
+ }
332
+
333
+ // Usage
334
+ trackScreen('ProductDetail', { product_id: product.id, category: product.category });
335
+ // NOT: trackScreen('ProductDetail', { user_email: user.email }); ← PII leak
336
+ ```
337
+
338
+ ### API Call Tracking
339
+
340
+ ```typescript
341
+ // ✅ Full API instrumentation
342
+ async function trackedRequest(
343
+ method: string,
344
+ url: string,
345
+ options?: RequestInit
346
+ ): Promise<Response> {
347
+ const traceId = nanoid();
348
+ const startTime = Date.now();
349
+ const context = sessionManager.getContext();
350
+
351
+ try {
352
+ const response = await fetch(url, {
353
+ ...options,
354
+ headers: {
355
+ ...options?.headers,
356
+ 'X-Trace-Id': traceId, // Correlate with backend
357
+ 'X-Session-Id': context.session_id,
358
+ },
359
+ });
360
+
361
+ const duration = Date.now() - startTime;
362
+
363
+ metrics.histogram('api.duration', duration, {
364
+ ...context,
365
+ method,
366
+ endpoint: sanitizeUrl(url), // Remove user IDs from URL
367
+ status: String(response.status),
368
+ success: String(response.ok),
369
+ });
370
+
371
+ logger.info('API call completed', {
372
+ ...context,
373
+ trace_id: traceId,
374
+ method,
375
+ endpoint: sanitizeUrl(url),
376
+ status: response.status,
377
+ duration_ms: duration,
378
+ });
379
+
380
+ return response;
381
+
382
+ } catch (error) {
383
+ metrics.increment('api.error', {
384
+ ...context,
385
+ method,
386
+ endpoint: sanitizeUrl(url),
387
+ error_type: error instanceof Error ? error.name : 'unknown',
388
+ });
389
+
390
+ logger.error('API call failed', {
391
+ ...context,
392
+ trace_id: traceId,
393
+ method,
394
+ endpoint: sanitizeUrl(url),
395
+ error: error instanceof Error ? error.message : String(error),
396
+ });
397
+
398
+ throw error;
399
+ }
400
+ }
401
+
402
+ function sanitizeUrl(url: string): string {
403
+ // Remove UUIDs and numeric IDs from URLs
404
+ return url
405
+ .replace(/\/[0-9a-f-]{36}/g, '/:id') // UUIDs
406
+ .replace(/\/\d+/g, '/:id') // Numeric IDs
407
+ .replace(/\?.*$/, ''); // Query params
408
+ }
409
+ ```
410
+
411
+ ### Error Tracking
412
+
413
+ ```typescript
414
+ // ✅ Crash reporting with session context
415
+ function trackError(error: Error, context?: Record<string, string>) {
416
+ const sessionContext = sessionManager.getContext();
417
+
418
+ // Update session crash count
419
+ sessionManager.incrementCrashCount();
420
+
421
+ crashReporter.captureException(error, {
422
+ tags: {
423
+ ...sessionContext,
424
+ ...context,
425
+ },
426
+ extra: {
427
+ crash_count: sessionManager.getCrashCount(),
428
+ // Breadcrumbs from session
429
+ recent_screens: sessionManager.getRecentScreens(),
430
+ },
431
+ });
432
+
433
+ logger.error('Application error', {
434
+ ...sessionContext,
435
+ error_name: error.name,
436
+ error_message: error.message,
437
+ // No stack traces in production logs (can contain file paths with PII)
438
+ stack_hash: hashString(error.stack ?? ''),
439
+ });
440
+ }
441
+
442
+ // Global error boundary (React Native)
443
+ import { ErrorBoundary } from 'react-error-boundary';
444
+
445
+ function GlobalErrorBoundary({ children }: { children: React.ReactNode }) {
446
+ return (
447
+ <ErrorBoundary
448
+ onError={(error, info) => {
449
+ trackError(error, { component: info.componentStack?.split('\n')[1] ?? 'unknown' });
450
+ }}
451
+ fallback={<ErrorScreen />}
452
+ >
453
+ {children}
454
+ </ErrorBoundary>
455
+ );
456
+ }
457
+ ```
458
+
459
+ ### Performance Tracking
460
+
461
+ ```typescript
462
+ // ✅ Frame rate and rendering monitoring
463
+ import { PerformanceObserver } from 'react-native';
464
+
465
+ function trackRenderPerformance(componentName: string) {
466
+ return function <T extends React.ComponentType<any>>(WrappedComponent: T): T {
467
+ const displayName = componentName || WrappedComponent.displayName || 'Unknown';
468
+
469
+ function TrackedComponent(props: React.ComponentProps<T>) {
470
+ const renderStart = useRef(Date.now());
471
+ const context = sessionManager.getContext();
472
+
473
+ useEffect(() => {
474
+ const renderDuration = Date.now() - renderStart.current;
475
+
476
+ if (renderDuration > 16) { // > 1 frame at 60fps
477
+ metrics.histogram('render.duration', renderDuration, {
478
+ ...context,
479
+ component: displayName,
480
+ slow: String(renderDuration > 100), // > 6 frames
481
+ });
482
+ }
483
+ }, []);
484
+
485
+ return <WrappedComponent {...props} />;
486
+ }
487
+
488
+ TrackedComponent.displayName = `Tracked(${displayName})`;
489
+ return TrackedComponent as T;
490
+ };
491
+ }
492
+
493
+ // Usage
494
+ export default trackRenderPerformance('ProductList')(ProductListComponent);
495
+ ```
496
+
497
+ ---
498
+
499
+ ## Alerting Based on Sessions
500
+
501
+ ### Alert Patterns
502
+
503
+ ```typescript
504
+ // Alert: Crash rate spike
505
+ if (session.crash_count > 0) {
506
+ alerts.trigger('crash_detected', {
507
+ session_id: session.session_id,
508
+ app_version: session.app_version,
509
+ crash_count: session.crash_count,
510
+ platform: session.platform,
511
+ os_version: session.os_version,
512
+ });
513
+ }
514
+
515
+ // Alert: Long session with no screen changes (app hung?)
516
+ if (session.duration_ms > 10 * 60 * 1000 && getScreenCount() <= 1) {
517
+ alerts.trigger('potential_freeze', {
518
+ session_id: session.session_id,
519
+ duration_ms: session.duration_ms,
520
+ screen_count: getScreenCount(),
521
+ });
522
+ }
523
+
524
+ // Alert: High background time (battery killer?)
525
+ const backgroundRatio =
526
+ session.background_time_ms / session.duration_ms;
527
+ if (backgroundRatio > 0.5) {
528
+ logger.warn('High background activity', {
529
+ session_id: session.session_id,
530
+ background_ratio: backgroundRatio.toFixed(2),
531
+ background_ms: session.background_time_ms,
532
+ });
533
+ }
534
+ ```
535
+
536
+ ### Dashboard Queries
537
+
538
+ ```sql
539
+ -- Session-based queries (example for any analytics platform)
540
+
541
+ -- Active sessions by platform
542
+ SELECT platform, COUNT(DISTINCT session_id) as active_sessions
543
+ FROM sessions
544
+ WHERE started_at > NOW() - INTERVAL '1 hour'
545
+ GROUP BY platform;
546
+
547
+ -- Crash rate by app version
548
+ SELECT app_version,
549
+ COUNT(DISTINCT session_id) as total_sessions,
550
+ SUM(crash_count) as total_crashes,
551
+ ROUND(SUM(crash_count)::numeric / COUNT(DISTINCT session_id) * 100, 2) as crash_rate_pct
552
+ FROM sessions
553
+ GROUP BY app_version
554
+ ORDER BY app_version DESC;
555
+
556
+ -- Session duration distribution
557
+ SELECT
558
+ platform,
559
+ PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY duration_ms) as p50_ms,
560
+ PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration_ms) as p90_ms,
561
+ PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY duration_ms) as p99_ms
562
+ FROM sessions
563
+ WHERE duration_ms IS NOT NULL
564
+ GROUP BY platform;
565
+
566
+ -- Network type vs error rate
567
+ SELECT s.network_type,
568
+ COUNT(DISTINCT s.session_id) as sessions,
569
+ COUNT(e.error_id) as errors,
570
+ ROUND(COUNT(e.error_id)::numeric / COUNT(DISTINCT s.session_id), 2) as errors_per_session
571
+ FROM sessions s
572
+ LEFT JOIN errors e USING (session_id)
573
+ GROUP BY s.network_type;
574
+ ```
575
+
576
+ ---
577
+
578
+ ## Context Requirements Checklist
579
+
580
+ Every instrumented event MUST include:
581
+
582
+ ```
583
+ □ session_id (links everything together)
584
+ □ device_id (user journey across sessions)
585
+ □ app_version (for regression detection)
586
+ □ platform (iOS vs Android differences)
587
+ □ timestamp (for timeline reconstruction)
588
+
589
+ OPTIONAL but valuable:
590
+ □ screen_name (where in the app)
591
+ □ network_type (wifi vs cellular behavior differences)
592
+ □ os_version (OS-specific bug detection)
593
+ □ user_tier (free vs premium behavior)
594
+ ```
595
+
596
+ Events MUST NOT include:
597
+ ```
598
+ ✗ user_email (PII — see anti-patterns.md)
599
+ ✗ user_name (PII)
600
+ ✗ phone_number (PII)
601
+ ✗ raw_user_id (replace with hashed or session_id)
602
+ ✗ full URL params (may contain tokens)
603
+ ✗ auth_token (security risk)
604
+ ```
605
+
606
+ ---
607
+
608
+ ## Observability Stack Recommendations
609
+
610
+ ### React Native
611
+
612
+ | Tool | Purpose | Integration |
613
+ |------|---------|-------------|
614
+ | **Sentry** | Crash reporting + error tracking | `@sentry/react-native` |
615
+ | **Datadog RUM** | Real User Monitoring | `@datadog/mobile-react-native` |
616
+ | **Firebase Crashlytics** | Crash reporting (free) | `@react-native-firebase/crashlytics` |
617
+ | **Firebase Analytics** | Event tracking (free) | `@react-native-firebase/analytics` |
618
+ | **Segment** | CDP (routes to other tools) | `@segment/analytics-react-native` |
619
+ | **New Relic Mobile** | Full observability | `newrelic-react-native-agent` |
620
+
621
+ ### Flutter
622
+
623
+ | Tool | Purpose | Integration |
624
+ |------|---------|-------------|
625
+ | **Sentry** | Crash reporting + error tracking | `sentry_flutter` |
626
+ | **Firebase Crashlytics** | Crash reporting (free) | `firebase_crashlytics` |
627
+ | **Firebase Analytics** | Event tracking (free) | `firebase_analytics` |
628
+ | **Datadog** | Full observability | `datadog_flutter_plugin` |
629
+ | **Segment** | CDP | `analytics` (Flutter) |
630
+ | **OpenTelemetry** | Vendor-neutral | `opentelemetry_dart` |
631
+
632
+ ---
633
+
634
+ ## Session-Aware Testing
635
+
636
+ ```typescript
637
+ // tests/observability/session.test.ts
638
+ describe('SessionManager', () => {
639
+ it('enriches all events with session context', () => {
640
+ const session = await sessionManager.startSession();
641
+
642
+ const rawEvent = { event: 'button_tap', button: 'subscribe' };
643
+ const enriched = sessionManager.enrichEvent(rawEvent);
644
+
645
+ expect(enriched).toMatchObject({
646
+ event: 'button_tap',
647
+ button: 'subscribe',
648
+ session_id: session.session_id,
649
+ device_id: expect.any(String),
650
+ app_version: expect.any(String),
651
+ platform: expect.stringMatching(/^(ios|android)$/),
652
+ timestamp: expect.any(Number),
653
+ });
654
+ });
655
+
656
+ it('starts a new session after 30 min background', async () => {
657
+ const session1 = await sessionManager.startSession();
658
+
659
+ // Simulate 31 minutes in background
660
+ jest.advanceTimersByTime(31 * 60 * 1000);
661
+
662
+ // App comes back to foreground
663
+ simulateAppState('active');
664
+
665
+ const session2 = sessionManager.getCurrentSession();
666
+ expect(session2.session_id).not.toBe(session1.session_id);
667
+ expect(session2.previous_session_id).toBe(session1.session_id);
668
+ });
669
+ });
670
+ ```
671
+
672
+ ---
673
+
674
+ ## Summary
675
+
676
+ **Sessions are the fourth pillar because:**
677
+ 1. **Metrics** tell you WHAT happened (count, duration, rate)
678
+ 2. **Logs** tell you WHAT was happening at a moment (details)
679
+ 3. **Traces** tell you HOW it happened (call chain)
680
+ 4. **Sessions** tell you WHO experienced it and WHEN in their journey
681
+
682
+ Without sessions:
683
+ - "Error spike at 2pm" → Can't tell if 1 user or 1000 users
684
+ - "API timeout" → Can't tell if it always happens or only on cellular
685
+
686
+ With sessions:
687
+ - "Error spike at 2pm" → 47 unique sessions, all on iOS 17.1, v2.1.3
688
+ - "API timeout" → 89% occur on 3G sessions, not WiFi