Comprehensive logging system for debugging update issues with structured log entries, filtering capabilities, and management functions for troubleshooting update problems.
Functions for reading and managing update-related log entries from the native update system.
/**
* Retrieves the most recent expo-updates log entries.
* @param maxAge Sets the max age of retrieved log entries in milliseconds. Default: 3600000 ms (1 hour)
* @returns Promise that fulfills with an array of UpdatesLogEntry objects
* @throws Error if there is an unexpected error in retrieving the logs
*/
function readLogEntriesAsync(maxAge?: number): Promise<UpdatesLogEntry[]>;
/**
* Clears existing expo-updates log entries.
* Note: For now, this operation does nothing on the client. Once log persistence
* has been implemented, this operation will actually remove existing logs.
* @returns Promise that fulfills if the clear operation was successful
* @throws Error if there is an unexpected error in clearing the logs
*/
function clearLogEntriesAsync(): Promise<void>;
interface UpdatesLogEntry {
/** The time the log was written, in milliseconds since Jan 1 1970 UTC */
timestamp: number;
/** The log entry message */
message: string;
/** One of the defined code values for expo-updates log entries */
code: UpdatesLogEntryCode;
/** One of the defined log level or severity values */
level: UpdatesLogEntryLevel;
/** If present, the unique ID of an update associated with this log entry */
updateId?: string;
/** If present, the unique ID or hash of an asset associated with this log entry */
assetId?: string;
/** If present, an Android or iOS native stack trace associated with this log entry */
stacktrace?: string[];
}
enum UpdatesLogEntryCode {
NONE = 'None',
NO_UPDATES_AVAILABLE = 'NoUpdatesAvailable',
UPDATE_ASSETS_NOT_AVAILABLE = 'UpdateAssetsNotAvailable',
UPDATE_SERVER_UNREACHABLE = 'UpdateServerUnreachable',
UPDATE_HAS_INVALID_SIGNATURE = 'UpdateHasInvalidSignature',
UPDATE_CODE_SIGNING_ERROR = 'UpdateCodeSigningError',
UPDATE_FAILED_TO_LOAD = 'UpdateFailedToLoad',
ASSETS_FAILED_TO_LOAD = 'AssetsFailedToLoad',
JS_RUNTIME_ERROR = 'JSRuntimeError',
INITIALIZATION_ERROR = 'InitializationError',
UNKNOWN = 'Unknown'
}
enum UpdatesLogEntryLevel {
TRACE = 'trace',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
FATAL = 'fatal'
}Usage Examples:
import * as Updates from "expo-updates";
// Basic log reading
const logs = await Updates.readLogEntriesAsync();
console.log(`Found ${logs.length} log entries`);
logs.forEach(log => {
console.log(`[${log.level.toUpperCase()}] ${log.message}`);
});
// Read logs from last 30 minutes
const thirtyMinutes = 30 * 60 * 1000;
const recentLogs = await Updates.readLogEntriesAsync(thirtyMinutes);
// Filter by log level
const errorLogs = recentLogs.filter(log =>
log.level === Updates.UpdatesLogEntryLevel.ERROR ||
log.level === Updates.UpdatesLogEntryLevel.FATAL
);
// Filter by specific update
const updateId = "12345-abcde";
const updateSpecificLogs = recentLogs.filter(log =>
log.updateId === updateId
);
// Clear all logs
await Updates.clearLogEntriesAsync();
console.log("Logs cleared");Utilities for analyzing and processing log entries for debugging and monitoring.
// Log analysis utility
class UpdatesLogAnalyzer {
async getLogsSummary(maxAge: number = 3600000): Promise<LogSummary> {
const logs = await Updates.readLogEntriesAsync(maxAge);
const summary: LogSummary = {
totalEntries: logs.length,
byLevel: {},
byCode: {},
errorCount: 0,
warningCount: 0,
updates: new Set(),
assets: new Set(),
timeRange: {
earliest: null,
latest: null
}
};
logs.forEach(log => {
// Count by level
summary.byLevel[log.level] = (summary.byLevel[log.level] || 0) + 1;
// Count by code
summary.byCode[log.code] = (summary.byCode[log.code] || 0) + 1;
// Count errors and warnings
if (log.level === Updates.UpdatesLogEntryLevel.ERROR ||
log.level === Updates.UpdatesLogEntryLevel.FATAL) {
summary.errorCount++;
}
if (log.level === Updates.UpdatesLogEntryLevel.WARN) {
summary.warningCount++;
}
// Track unique updates and assets
if (log.updateId) summary.updates.add(log.updateId);
if (log.assetId) summary.assets.add(log.assetId);
// Track time range
if (!summary.timeRange.earliest || log.timestamp < summary.timeRange.earliest) {
summary.timeRange.earliest = log.timestamp;
}
if (!summary.timeRange.latest || log.timestamp > summary.timeRange.latest) {
summary.timeRange.latest = log.timestamp;
}
});
return summary;
}
async getErrorsForUpdate(updateId: string): Promise<UpdatesLogEntry[]> {
const logs = await Updates.readLogEntriesAsync();
return logs.filter(log =>
log.updateId === updateId &&
(log.level === Updates.UpdatesLogEntryLevel.ERROR ||
log.level === Updates.UpdatesLogEntryLevel.FATAL)
);
}
async getNetworkErrors(): Promise<UpdatesLogEntry[]> {
const logs = await Updates.readLogEntriesAsync();
return logs.filter(log =>
log.code === Updates.UpdatesLogEntryCode.UPDATE_SERVER_UNREACHABLE ||
log.code === Updates.UpdatesLogEntryCode.UPDATE_ASSETS_NOT_AVAILABLE
);
}
formatLogEntry(log: UpdatesLogEntry): string {
const timestamp = new Date(log.timestamp).toISOString();
const level = log.level.toUpperCase().padEnd(5);
const code = log.code !== Updates.UpdatesLogEntryCode.NONE ? `[${log.code}] ` : '';
const updateInfo = log.updateId ? ` (Update: ${log.updateId})` : '';
const assetInfo = log.assetId ? ` (Asset: ${log.assetId})` : '';
return `${timestamp} ${level} ${code}${log.message}${updateInfo}${assetInfo}`;
}
}
interface LogSummary {
totalEntries: number;
byLevel: Record<string, number>;
byCode: Record<string, number>;
errorCount: number;
warningCount: number;
updates: Set<string>;
assets: Set<string>;
timeRange: {
earliest: number | null;
latest: number | null;
};
}
// Usage
const analyzer = new UpdatesLogAnalyzer();
// Get comprehensive summary
const summary = await analyzer.getLogsSummary();
console.log(`Total logs: ${summary.totalEntries}`);
console.log(`Errors: ${summary.errorCount}, Warnings: ${summary.warningCount}`);
console.log(`Updates involved: ${summary.updates.size}`);
// Check for specific update errors
const updateErrors = await analyzer.getErrorsForUpdate("update-id-123");
updateErrors.forEach(error => {
console.log(analyzer.formatLogEntry(error));
});
// Check network connectivity issues
const networkErrors = await analyzer.getNetworkErrors();
if (networkErrors.length > 0) {
console.log("Network connectivity issues detected:");
networkErrors.forEach(error => {
console.log(analyzer.formatLogEntry(error));
});
}React components and hooks for displaying and monitoring update logs in the UI.
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, Button, RefreshControl } from 'react-native';
import * as Updates from 'expo-updates';
// Hook for log monitoring
function useUpdateLogs(maxAge: number = 3600000, refreshInterval: number = 30000) {
const [logs, setLogs] = useState<Updates.UpdatesLogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchLogs = async () => {
try {
setError(null);
const entries = await Updates.readLogEntriesAsync(maxAge);
setLogs(entries);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch logs');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchLogs();
const interval = setInterval(fetchLogs, refreshInterval);
return () => clearInterval(interval);
}, [maxAge, refreshInterval]);
const clearLogs = async () => {
try {
await Updates.clearLogEntriesAsync();
await fetchLogs(); // Refresh after clearing
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to clear logs');
}
};
return { logs, loading, error, refresh: fetchLogs, clearLogs };
}
// Log viewer component
function UpdatesLogViewer() {
const { logs, loading, error, refresh, clearLogs } = useUpdateLogs();
const [filter, setFilter] = useState<Updates.UpdatesLogEntryLevel | 'all'>('all');
const filteredLogs = filter === 'all'
? logs
: logs.filter(log => log.level === filter);
const formatTimestamp = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};
const getLogColor = (level: Updates.UpdatesLogEntryLevel) => {
switch (level) {
case Updates.UpdatesLogEntryLevel.ERROR:
case Updates.UpdatesLogEntryLevel.FATAL:
return '#ff4444';
case Updates.UpdatesLogEntryLevel.WARN:
return '#ffaa00';
case Updates.UpdatesLogEntryLevel.INFO:
return '#0088ff';
default:
return '#666666';
}
};
if (loading) {
return <Text>Loading logs...</Text>;
}
if (error) {
return (
<View>
<Text style={{color: 'red'}}>Error: {error}</Text>
<Button title="Retry" onPress={refresh} />
</View>
);
}
return (
<View style={{ flex: 1 }}>
<View style={{ flexDirection: 'row', padding: 10 }}>
<Button title="Refresh" onPress={refresh} />
<Button title="Clear Logs" onPress={clearLogs} />
</View>
<View style={{ flexDirection: 'row', padding: 10 }}>
<Text>Filter: </Text>
{(['all', 'error', 'warn', 'info', 'debug'] as const).map(level => (
<Button
key={level}
title={level.toUpperCase()}
onPress={() => setFilter(level as any)}
/>
))}
</View>
<ScrollView
style={{ flex: 1 }}
refreshControl={<RefreshControl refreshing={loading} onRefresh={refresh} />}
>
{filteredLogs.map((log, index) => (
<View
key={index}
style={{
padding: 10,
borderBottomWidth: 1,
borderBottomColor: '#eee'
}}
>
<Text style={{ fontSize: 12, color: '#666' }}>
{formatTimestamp(log.timestamp)}
</Text>
<Text style={{
fontWeight: 'bold',
color: getLogColor(log.level)
}}>
[{log.level.toUpperCase()}] {log.code !== Updates.UpdatesLogEntryCode.NONE && `[${log.code}]`}
</Text>
<Text>{log.message}</Text>
{log.updateId && (
<Text style={{ fontSize: 12, color: '#666' }}>
Update: {log.updateId}
</Text>
)}
{log.assetId && (
<Text style={{ fontSize: 12, color: '#666' }}>
Asset: {log.assetId}
</Text>
)}
{log.stacktrace && (
<View style={{ marginTop: 5 }}>
<Text style={{ fontWeight: 'bold' }}>Stack Trace:</Text>
{log.stacktrace.map((line, i) => (
<Text key={i} style={{ fontSize: 10, fontFamily: 'monospace' }}>
{line}
</Text>
))}
</View>
)}
</View>
))}
{filteredLogs.length === 0 && (
<Text style={{ textAlign: 'center', padding: 20, color: '#666' }}>
No log entries found
</Text>
)}
</ScrollView>
</View>
);
}
// Debug information panel
function UpdatesDebugPanel() {
const [summary, setSummary] = useState<LogSummary | null>(null);
useEffect(() => {
const analyzer = new UpdatesLogAnalyzer();
analyzer.getLogsSummary().then(setSummary);
}, []);
const exportLogs = async () => {
try {
const logs = await Updates.readLogEntriesAsync();
const analyzer = new UpdatesLogAnalyzer();
const formatted = logs.map(log => analyzer.formatLogEntry(log)).join('\n');
// Save to file or share
await shareTextFile('updates-logs.txt', formatted);
} catch (error) {
console.error('Failed to export logs:', error);
}
};
return (
<View style={{ padding: 20 }}>
<Text style={{ fontSize: 18, fontWeight: 'bold' }}>Updates Debug Info</Text>
<View style={{ marginTop: 10 }}>
<Text>Updates Enabled: {Updates.isEnabled ? 'Yes' : 'No'}</Text>
<Text>Current Update ID: {Updates.updateId || 'None'}</Text>
<Text>Channel: {Updates.channel || 'None'}</Text>
<Text>Runtime Version: {Updates.runtimeVersion || 'None'}</Text>
<Text>Embedded Launch: {Updates.isEmbeddedLaunch ? 'Yes' : 'No'}</Text>
<Text>Emergency Launch: {Updates.isEmergencyLaunch ? 'Yes' : 'No'}</Text>
</View>
{summary && (
<View style={{ marginTop: 10 }}>
<Text style={{ fontWeight: 'bold' }}>Log Summary:</Text>
<Text>Total Entries: {summary.totalEntries}</Text>
<Text>Errors: {summary.errorCount}</Text>
<Text>Warnings: {summary.warningCount}</Text>
<Text>Updates: {summary.updates.size}</Text>
<Text>Assets: {summary.assets.size}</Text>
</View>
)}
<Button title="Export Logs" onPress={exportLogs} />
</View>
);
}Typical log patterns and what they indicate for troubleshooting update issues.
// Log pattern analysis
function analyzeLogPatterns(logs: Updates.UpdatesLogEntry[]): DiagnosticReport {
const report: DiagnosticReport = {
issues: [],
recommendations: []
};
// Check for network connectivity issues
const networkErrors = logs.filter(log =>
log.code === Updates.UpdatesLogEntryCode.UPDATE_SERVER_UNREACHABLE
);
if (networkErrors.length > 0) {
report.issues.push("Network connectivity issues detected");
report.recommendations.push("Check internet connection and server availability");
}
// Check for signature validation issues
const signatureErrors = logs.filter(log =>
log.code === Updates.UpdatesLogEntryCode.UPDATE_HAS_INVALID_SIGNATURE ||
log.code === Updates.UpdatesLogEntryCode.UPDATE_CODE_SIGNING_ERROR
);
if (signatureErrors.length > 0) {
report.issues.push("Code signing validation failures");
report.recommendations.push("Verify update signing configuration and certificates");
}
// Check for asset loading issues
const assetErrors = logs.filter(log =>
log.code === Updates.UpdatesLogEntryCode.UPDATE_ASSETS_NOT_AVAILABLE ||
log.code === Updates.UpdatesLogEntryCode.ASSETS_FAILED_TO_LOAD
);
if (assetErrors.length > 0) {
report.issues.push("Asset loading failures");
report.recommendations.push("Check asset availability and network connectivity");
}
// Check for runtime errors
const runtimeErrors = logs.filter(log =>
log.code === Updates.UpdatesLogEntryCode.JS_RUNTIME_ERROR
);
if (runtimeErrors.length > 0) {
report.issues.push("JavaScript runtime errors");
report.recommendations.push("Review update code for runtime issues");
}
return report;
}
interface DiagnosticReport {
issues: string[];
recommendations: string[];
}
// Usage
const logs = await Updates.readLogEntriesAsync();
const report = analyzeLogPatterns(logs);
console.log("Issues found:", report.issues);
console.log("Recommendations:", report.recommendations);