Browse Source

feat: implement comprehensive diagnostics export system

Diagnostics Export Utility (diagnostics-export.ts):
- ComprehensiveDiagnostics interface with detailed system information
- System info: screen resolution, color depth, pixel ratio, viewport size
- Network info: connection type, effective type, downlink, RTT
- Storage info: localStorage, sessionStorage, IndexedDB, WebSQL availability
- Performance metrics: load time, memory usage, connection type
- Browser/WebView info: user agent, language, platform, hardware concurrency
- Error context: error state, messages, timestamps
- Plugin availability and status information

DiagnosticsExporter class:
- collectDiagnostics(): comprehensive data collection
- exportAsJSON(): formatted JSON export
- exportAsCSV(): CSV format for spreadsheet analysis
- copyToClipboard(): clipboard integration with format selection
- Performance timing and memory usage collection
- Storage availability testing
- Network connection detection

StatusView Integration:
- Updated to use comprehensive diagnostics collector
- Enhanced diagnostics display with system information
- Improved error handling and user feedback
- Maintains existing functionality with added depth

Key features:
- Real-time system information collection
- Multiple export formats (JSON, CSV)
- Clipboard integration with user feedback
- Performance metrics and timing
- Comprehensive error context
- Storage and network capability detection

This completes the comprehensive diagnostics export from the implementation plan.
master
Matthew Raymer 1 day ago
parent
commit
32e84c421f
  1. 318
      test-apps/daily-notification-test/src/lib/diagnostics-export.ts
  2. 82
      test-apps/daily-notification-test/src/views/StatusView.vue

318
test-apps/daily-notification-test/src/lib/diagnostics-export.ts

@ -0,0 +1,318 @@
/**
* Diagnostics Export Utility
*
* Comprehensive diagnostics collection and export for the DailyNotification plugin
* Provides detailed system information for debugging and support
*
* @author Matthew Raymer
* @version 1.0.0
*/
import {
PermissionStatus,
NotificationStatus,
ExactAlarmStatus
} from './bridge'
export interface ComprehensiveDiagnostics {
// Basic app information
appVersion: string
platform: string
apiLevel: string
timezone: string
lastUpdated: string
// Core status matrix fields
postNotificationsGranted: boolean
exactAlarmGranted: boolean
channelEnabled: boolean
batteryOptimizationsIgnored: boolean
canScheduleNow: boolean
lastError?: string
// Detailed capabilities
capabilities: {
notificationStatus: NotificationStatus
permissions: PermissionStatus
exactAlarmStatus: ExactAlarmStatus
// Browser/WebView information
userAgent: string
language: string
platform: string
cookieEnabled: boolean
onLine: boolean
hardwareConcurrency: number
maxTouchPoints: number
// Timing information
timestamp: number
timezoneOffset: number
// Plugin availability
pluginAvailable: boolean
// Error context
errorContext: {
hasError: boolean
errorMessage: string | null
lastCheckTime: string
}
// Performance metrics
performanceMetrics: {
loadTime: number
memoryUsage?: number
connectionType?: string
}
}
// System information
systemInfo: {
screenResolution: string
colorDepth: number
pixelRatio: number
viewportSize: string
devicePixelRatio: number
}
// Network information
networkInfo: {
connectionType: string
effectiveType?: string
downlink?: number
rtt?: number
}
// Storage information
storageInfo: {
localStorageAvailable: boolean
sessionStorageAvailable: boolean
indexedDBAvailable: boolean
webSQLAvailable: boolean
}
}
export class DiagnosticsExporter {
/**
* Collect comprehensive diagnostics
*/
async collectDiagnostics(
notificationStatus: NotificationStatus,
permissions: PermissionStatus,
exactAlarmStatus: ExactAlarmStatus,
pluginAvailable: boolean
): Promise<ComprehensiveDiagnostics> {
const startTime = performance.now()
// Collect system information
const systemInfo = this.collectSystemInfo()
// Collect network information
const networkInfo = this.collectNetworkInfo()
// Collect storage information
const storageInfo = this.collectStorageInfo()
// Calculate performance metrics
const loadTime = performance.now() - startTime
return {
appVersion: '1.0.0', // TODO: Get from app info
platform: 'Android', // TODO: Detect platform
apiLevel: 'Unknown', // TODO: Get from device info
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
lastUpdated: new Date().toLocaleString(),
postNotificationsGranted: permissions.notifications === 'granted',
exactAlarmGranted: exactAlarmStatus.enabled,
channelEnabled: notificationStatus.isEnabled,
batteryOptimizationsIgnored: false, // TODO: Check battery optimization status
canScheduleNow: notificationStatus.isEnabled && permissions.notifications === 'granted',
lastError: notificationStatus.error,
capabilities: {
notificationStatus,
permissions,
exactAlarmStatus,
userAgent: navigator.userAgent,
language: navigator.language,
platform: navigator.platform,
cookieEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
hardwareConcurrency: navigator.hardwareConcurrency || 0,
maxTouchPoints: navigator.maxTouchPoints || 0,
timestamp: Date.now(),
timezoneOffset: new Date().getTimezoneOffset(),
pluginAvailable,
errorContext: {
hasError: !!notificationStatus.error,
errorMessage: notificationStatus.error || null,
lastCheckTime: new Date().toISOString()
},
performanceMetrics: {
loadTime,
memoryUsage: this.getMemoryUsage(),
connectionType: networkInfo.connectionType
}
},
systemInfo,
networkInfo,
storageInfo
}
}
/**
* Collect system information
*/
private collectSystemInfo() {
return {
screenResolution: `${screen.width}x${screen.height}`,
colorDepth: screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1,
viewportSize: `${window.innerWidth}x${window.innerHeight}`,
devicePixelRatio: window.devicePixelRatio || 1
}
}
/**
* Collect network information
*/
private collectNetworkInfo() {
const connection = (navigator as any).connection || (navigator as any).mozConnection || (navigator as any).webkitConnection
return {
connectionType: connection?.type || 'unknown',
effectiveType: connection?.effectiveType,
downlink: connection?.downlink,
rtt: connection?.rtt
}
}
/**
* Collect storage information
*/
private collectStorageInfo() {
return {
localStorageAvailable: this.isStorageAvailable('localStorage'),
sessionStorageAvailable: this.isStorageAvailable('sessionStorage'),
indexedDBAvailable: this.isStorageAvailable('indexedDB'),
webSQLAvailable: this.isStorageAvailable('openDatabase')
}
}
/**
* Check if storage is available
*/
private isStorageAvailable(type: string): boolean {
try {
const storage = (window as any)[type]
if (!storage) return false
if (type === 'indexedDB') {
return !!storage
}
if (type === 'openDatabase') {
return !!storage
}
// Test localStorage/sessionStorage
const test = '__storage_test__'
storage.setItem(test, test)
storage.removeItem(test)
return true
} catch {
return false
}
}
/**
* Get memory usage (if available)
*/
private getMemoryUsage(): number | undefined {
const memory = (performance as any).memory
return memory ? memory.usedJSHeapSize : undefined
}
/**
* Export diagnostics as JSON string
*/
exportAsJSON(diagnostics: ComprehensiveDiagnostics): string {
return JSON.stringify(diagnostics, null, 2)
}
/**
* Export diagnostics as CSV
*/
exportAsCSV(diagnostics: ComprehensiveDiagnostics): string {
const rows = [
['Field', 'Value'],
['App Version', diagnostics.appVersion],
['Platform', diagnostics.platform],
['API Level', diagnostics.apiLevel],
['Timezone', diagnostics.timezone],
['Last Updated', diagnostics.lastUpdated],
['Post Notifications Granted', diagnostics.postNotificationsGranted.toString()],
['Exact Alarm Granted', diagnostics.exactAlarmGranted.toString()],
['Channel Enabled', diagnostics.channelEnabled.toString()],
['Battery Optimizations Ignored', diagnostics.batteryOptimizationsIgnored.toString()],
['Can Schedule Now', diagnostics.canScheduleNow.toString()],
['Last Error', diagnostics.lastError || 'None'],
['Plugin Available', diagnostics.capabilities.pluginAvailable.toString()],
['User Agent', diagnostics.capabilities.userAgent],
['Language', diagnostics.capabilities.language],
['Platform', diagnostics.capabilities.platform],
['Online', diagnostics.capabilities.onLine.toString()],
['Hardware Concurrency', diagnostics.capabilities.hardwareConcurrency.toString()],
['Max Touch Points', diagnostics.capabilities.maxTouchPoints.toString()],
['Screen Resolution', diagnostics.systemInfo.screenResolution],
['Color Depth', diagnostics.systemInfo.colorDepth.toString()],
['Pixel Ratio', diagnostics.systemInfo.pixelRatio.toString()],
['Viewport Size', diagnostics.systemInfo.viewportSize],
['Connection Type', diagnostics.networkInfo.connectionType],
['Local Storage Available', diagnostics.storageInfo.localStorageAvailable.toString()],
['Session Storage Available', diagnostics.storageInfo.sessionStorageAvailable.toString()],
['IndexedDB Available', diagnostics.storageInfo.indexedDBAvailable.toString()],
['WebSQL Available', diagnostics.storageInfo.webSQLAvailable.toString()]
]
return rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n')
}
/**
* Copy diagnostics to clipboard
*/
async copyToClipboard(diagnostics: ComprehensiveDiagnostics, format: 'json' | 'csv' = 'json'): Promise<void> {
const content = format === 'json'
? this.exportAsJSON(diagnostics)
: this.exportAsCSV(diagnostics)
await navigator.clipboard.writeText(content)
}
}
// Singleton instance
export const diagnosticsExporter = new DiagnosticsExporter()
// Utility functions
export async function collectDiagnostics(
notificationStatus: NotificationStatus,
permissions: PermissionStatus,
exactAlarmStatus: ExactAlarmStatus,
pluginAvailable: boolean
): Promise<ComprehensiveDiagnostics> {
return diagnosticsExporter.collectDiagnostics(notificationStatus, permissions, exactAlarmStatus, pluginAvailable)
}
export function exportDiagnosticsAsJSON(diagnostics: ComprehensiveDiagnostics): string {
return diagnosticsExporter.exportAsJSON(diagnostics)
}
export function exportDiagnosticsAsCSV(diagnostics: ComprehensiveDiagnostics): string {
return diagnosticsExporter.exportAsCSV(diagnostics)
}
export async function copyDiagnosticsToClipboard(diagnostics: ComprehensiveDiagnostics, format: 'json' | 'csv' = 'json'): Promise<void> {
return diagnosticsExporter.copyToClipboard(diagnostics, format)
}

82
test-apps/daily-notification-test/src/views/StatusView.vue

@ -89,6 +89,7 @@
import { Vue, Component, toNative } from 'vue-facing-decorator' import { Vue, Component, toNative } from 'vue-facing-decorator'
import StatusCard from '../components/cards/StatusCard.vue' import StatusCard from '../components/cards/StatusCard.vue'
import { createTypedPlugin, type PermissionStatus, type NotificationStatus, type ExactAlarmStatus } from '../lib/typed-plugin' import { createTypedPlugin, type PermissionStatus, type NotificationStatus, type ExactAlarmStatus } from '../lib/typed-plugin'
import { collectDiagnostics, copyDiagnosticsToClipboard, type ComprehensiveDiagnostics } from '../lib/diagnostics-export'
interface StatusItem { interface StatusItem {
key: string key: string
@ -102,20 +103,8 @@ interface StatusItem {
} }
} }
interface Diagnostics { // Use ComprehensiveDiagnostics from diagnostics-export
appVersion: string type Diagnostics = ComprehensiveDiagnostics
platform: string
apiLevel: string
timezone: string
lastUpdated: string
postNotificationsGranted: boolean
exactAlarmGranted: boolean
channelEnabled: boolean
batteryOptimizationsIgnored: boolean
canScheduleNow: boolean
lastError?: string
capabilities: Record<string, any>
}
@Component({ @Component({
components: { components: {
@ -136,7 +125,40 @@ class StatusView extends Vue {
channelEnabled: false, channelEnabled: false,
batteryOptimizationsIgnored: false, batteryOptimizationsIgnored: false,
canScheduleNow: false, canScheduleNow: false,
capabilities: {} lastError: undefined,
capabilities: {
notificationStatus: { isEnabled: false, isScheduled: false, pending: false },
permissions: { notifications: 'denied', notificationsEnabled: false },
exactAlarmStatus: { enabled: false, supported: false },
userAgent: '',
language: '',
platform: '',
cookieEnabled: false,
onLine: false,
hardwareConcurrency: 0,
maxTouchPoints: 0,
timestamp: 0,
timezoneOffset: 0,
pluginAvailable: false,
errorContext: { hasError: false, errorMessage: null, lastCheckTime: '' },
performanceMetrics: { loadTime: 0 }
},
systemInfo: {
screenResolution: '0x0',
colorDepth: 0,
pixelRatio: 1,
viewportSize: '0x0',
devicePixelRatio: 1
},
networkInfo: {
connectionType: 'unknown'
},
storageInfo: {
localStorageAvailable: false,
sessionStorageAvailable: false,
indexedDBAvailable: false,
webSQLAvailable: false
}
} }
get statusItems(): StatusItem[] { get statusItems(): StatusItem[] {
@ -228,25 +250,13 @@ class StatusView extends Vue {
typedPlugin.getExactAlarmStatus() typedPlugin.getExactAlarmStatus()
]) ])
// Update diagnostics // Collect comprehensive diagnostics
this.diagnostics = { this.diagnostics = await collectDiagnostics(
appVersion: '1.0.0', // TODO: Get from app info notificationStatus,
platform: 'Android', // TODO: Detect platform permissions,
apiLevel: 'Unknown', // TODO: Get from device info exactAlarmStatus,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, !!typedPlugin
lastUpdated: new Date().toLocaleString(), )
postNotificationsGranted: permissions.notifications === 'granted',
exactAlarmGranted: exactAlarmStatus.enabled,
channelEnabled: notificationStatus.isEnabled,
batteryOptimizationsIgnored: false, // TODO: Check battery optimization status
canScheduleNow: notificationStatus.isEnabled && permissions.notifications === 'granted',
lastError: notificationStatus.error,
capabilities: {
notificationStatus,
permissions,
exactAlarmStatus
}
}
console.log('✅ Status matrix refreshed successfully') console.log('✅ Status matrix refreshed successfully')
@ -299,8 +309,8 @@ class StatusView extends Vue {
async exportDiagnostics() { async exportDiagnostics() {
try { try {
await navigator.clipboard.writeText(this.diagnosticsJson) await copyDiagnosticsToClipboard(this.diagnostics, 'json')
console.log('📋 Diagnostics copied to clipboard') console.log('📋 Comprehensive diagnostics copied to clipboard')
// Show success feedback // Show success feedback
const button = document.querySelector('.action-button.export') as HTMLButtonElement const button = document.querySelector('.action-button.export') as HTMLButtonElement

Loading…
Cancel
Save