or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-config.mdapp-state-config.mdindex.mdlogging-debugging.mdreact-integration.mdupdate-management.md
tile.json

logging-debugging.mddocs/

Logging and Debugging

Comprehensive logging system for debugging update issues with structured log entries, filtering capabilities, and management functions for troubleshooting update problems.

Capabilities

Log Entry Management

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");

Advanced Log Analysis

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 Integration for Log Monitoring

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>
  );
}

Common Log Patterns and Troubleshooting

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);