google_sheets_logger 1.0.1
google_sheets_logger: ^1.0.1 copied to clipboard
A lightweight, serverless error tracking system for Flutter Web that routes crashes and API failures directly into a Google Sheet using Google Apps Script.
Google Sheets Logger #
A lightweight, serverless error tracking system for Flutter Web that routes crashes and API failures directly into a Google Sheet using Google Apps Script. No backend server required.
Features #
- 🚀 Serverless - No backend server needed, uses Google Apps Script
- 🌐 Flutter Web Optimized - Uses
text/plaincontent-type to bypass CORS preflight - 🌍 Auto IP Detection - Automatically captures client IP via ipify
- 🛡️ Error Resilient - Silently fails if logging fails (never crashes your app)
- 🔧 Zero Config Backend - Just deploy a Google Apps Script
- 📱 Cross-platform - Works on Flutter Web, iOS, Android, and Desktop
Getting Started #
1. Add Dependency #
dependencies:
google_sheets_logger: ^1.0.0
2. Create a Google Apps Script #
- Go to script.google.com
- Create a new project
- Paste this code:
function doPost(e) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = JSON.parse(e.postData.contents);
const row = [
new Date(), // Timestamp
data.projectName, // Project name
data.environment, // Environment (dev/staging/prod)
data.ipAddress, // Client IP
data.errorType, // Error type (e.g., "API_ERROR", "CRASH")
data.endpoint || '', // API endpoint (if applicable)
data.errorCode || '', // Error code (if applicable)
data.errorMessage, // Error message
data.stackTrace || '' // Stack trace
];
sheet.appendRow(row);
return ContentService.createTextOutput(JSON.stringify({success: true}))
.setMimeType(ContentService.MimeType.JSON);
}
- Deploy → New Deployment → Web App
- Execute as: Me
- Who has access: Anyone
- Copy the Web App URL (ends with
/exec)
3. Initialize in main.dart #
import 'package:flutter/material.dart';
import 'package:google_sheets_logger/google_sheets_logger.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize BEFORE runApp()
await GoogleSheetsLogger.initialize(
scriptUrl: 'https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec',
projectName: 'My Flutter App',
environment: 'production', // or 'development', 'staging'
);
runApp(const MyApp());
}
4. Log Errors Manually #
try {
final response = await http.get(Uri.parse('https://api.example.com/data'));
if (response.statusCode != 200) {
throw Exception('API returned ${response.statusCode}');
}
} catch (e, stackTrace) {
await GoogleSheetsLogger.logError(
errorMessage: e.toString(),
errorType: 'API_ERROR',
endpoint: 'https://api.example.com/data',
errorCode: 'HTTP_${response?.statusCode}',
stackTrace: stackTrace.toString(),
);
// Show user-friendly error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to load data. Please try again.')),
);
}
5. Catch Uncaught Flutter Errors (Optional) #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GoogleSheetsLogger.initialize(
scriptUrl: 'https://script.google.com/macros/s/YOUR_SCRIPT_ID/exec',
projectName: 'My Flutter App',
environment: 'production',
);
// Catch Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) async {
await GoogleSheetsLogger.logError(
errorMessage: details.exceptionAsString(),
errorType: 'FLUTTER_FRAMEWORK_ERROR',
stackTrace: details.stack?.toString(),
);
FlutterError.presentError(details);
};
// Catch Dart async errors
PlatformDispatcher.instance.onError = (error, stack) async {
await GoogleSheetsLogger.logError(
errorMessage: error.toString(),
errorType: 'DART_ASYNC_ERROR',
stackTrace: stack.toString(),
);
return true;
};
runApp(const MyApp());
}
API Reference #
GoogleSheetsLogger.initialize() #
Initializes the logger. Must be called before runApp().
static Future<void> initialize({
required String scriptUrl, // Your Google Apps Script Web App URL
required String projectName, // Your project/app name
required String environment, // 'development' | 'staging' | 'production'
}) async
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
scriptUrl |
String |
Yes | Google Apps Script Web App URL (ends with /exec) |
projectName |
String |
Yes | Your project/app name for identification |
environment |
String |
Yes | Environment name (e.g., 'production', 'staging', 'development') |
GoogleSheetsLogger.logError() #
Manually log an error to Google Sheets.
static Future<void> logError({
required String errorMessage, // Human-readable error message
required String errorType, // Error category (e.g., 'API_ERROR', 'CRASH')
String? endpoint, // API endpoint that failed (optional)
String? errorCode, // Error code (e.g., 'HTTP_500', 'TIMEOUT')
String? stackTrace, // Stack trace string (optional)
}) async
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
errorMessage |
String |
Yes | The error message to log |
errorType |
String |
Yes | Category/type of error |
endpoint |
String? |
No | API endpoint URL if applicable |
errorCode |
String? |
No | HTTP status code or custom error code |
stackTrace |
String? |
No | Stack trace as string |
Suggested Error Types:
| Type | Use Case |
|---|---|
API_ERROR |
HTTP API failures |
NETWORK_ERROR |
Network connectivity issues |
PARSE_ERROR |
JSON parsing failures |
AUTH_ERROR |
Authentication/authorization failures |
VALIDATION_ERROR |
Input validation failures |
FLUTTER_FRAMEWORK_ERROR |
Flutter framework errors |
DART_ASYNC_ERROR |
Uncaught Dart async errors |
CRASH |
App crashes |
CUSTOM |
Custom error types |
Google Sheets Output Format #
The Google Sheet will receive rows with these columns:
| Column | Description |
|---|---|
| A | Timestamp (auto-generated) |
| B | Project Name |
| C | Environment |
| D | Client IP Address |
| E | Error Type |
| F | API Endpoint |
| G | Error Code |
| H | Error Message |
| I | Stack Trace |
Complete Example: main.dart #
import 'package:flutter/material.dart';
import 'package:google_sheets_logger/google_sheets_logger.dart';
import 'package:flutter/foundation.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize logger BEFORE runApp
await GoogleSheetsLogger.initialize(
scriptUrl: const String.fromEnvironment('GOOGLE_SCRIPT_URL'),
projectName: const String.fromEnvironment('PROJECT_NAME', defaultValue: 'MyApp'),
environment: const String.fromEnvironment('ENVIRONMENT', defaultValue: 'development'),
);
// Global error handlers (optional but recommended)
FlutterError.onError = (details) async {
await GoogleSheetsLogger.logError(
errorMessage: details.exceptionAsString(),
errorType: 'FLUTTER_FRAMEWORK_ERROR',
stackTrace: details.stack?.toString(),
);
FlutterError.presentError(details);
};
PlatformDispatcher.instance.onError = (error, stack) async {
await GoogleSheetsLogger.logError(
errorMessage: error.toString(),
errorType: 'DART_ASYNC_ERROR',
stackTrace: stack.toString(),
);
return true;
};
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Google Sheets Logger Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _status = 'Ready';
Future<void> _testApiError() async {
setState(() => _status = 'Testing API error...');
try {
// This will fail - invalid URL
await http.get(Uri.parse('https://httpstat.us/500'));
} catch (e, stack) {
await GoogleSheetsLogger.logError(
errorMessage: e.toString(),
errorType: 'API_ERROR',
endpoint: 'https://httpstat.us/500',
errorCode: 'HTTP_500',
stackTrace: stack.toString(),
);
setState(() => _status = 'Error logged! Check your Google Sheet.');
}
}
Future<void> _testCrash() async {
await GoogleSheetsLogger.logError(
errorMessage: 'Test crash from button press',
errorType: 'CRASH',
stackTrace: StackTrace.current.toString(),
);
setState(() => _status = 'Test crash logged!');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Google Sheets Logger Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_status, style: const TextStyle(fontSize: 18)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _testApiError,
child: const Text('Test API Error'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _testCrash,
child: const Text('Test Manual Crash Log'),
),
],
),
),
);
}
}
Environment Variables (Recommended) #
Use --dart-define for sensitive configuration:
flutter run --dart-define=GOOGLE_SCRIPT_URL=https://script.google.com/macros/s/XXX/exec \
--dart-define=PROJECT_NAME=MyApp \
--dart-define=ENVIRONMENT=production
await GoogleSheetsLogger.initialize(
scriptUrl: const String.fromEnvironment('GOOGLE_SCRIPT_URL'),
projectName: const String.fromEnvironment('PROJECT_NAME'),
environment: const String.fromEnvironment('ENVIRONMENT'),
);
Google Apps Script Setup Details #
Sheet Setup #
- Create a new Google Sheet
- Add headers in row 1:
Timestamp | Project | Environment | IP Address | Error Type | Endpoint | Error Code | Message | Stack Trace - Open Extensions → Apps Script
- Paste the
doPostfunction from Quick Start - Deploy as Web App (Anyone, Execute as Me)
CORS Note #
The library uses Content-Type: text/plain to avoid CORS preflight requests on Flutter Web. The Apps Script handles JSON parsing from e.postData.contents.
Security (Optional) #
Add a secret token validation in Apps Script for production:
function doPost(e) {
const secret = e.parameter.secret;
if (secret !== PropertiesService.getScriptProperties().getProperty('SECRET')) {
return ContentService.createTextOutput('Unauthorized').setMimeType(ContentService.MimeType.TEXT);
}
// ... rest of code
}
Error Handling Behavior #
| Scenario | Behavior |
|---|---|
| Not initialized | Logs debug message, returns silently |
| Network error | Catches silently, logs to debug console |
| CORS error | Catches silently (text/plain bypasses preflight) |
| Apps Script error | Catches silently, logs to debug console |
| App crash | Never crashes your app - fails silently |
Troubleshooting #
| Issue | Solution |
|---|---|
| "Not initialized" error | Call initialize() before runApp() |
| Errors not appearing in Sheet | Check Apps Script execution logs (Extensions → Apps Script → Executions) |
| CORS errors | Ensure Apps Script is deployed as "Anyone" access |
| IP shows "Unknown IP" | ipify.org may be blocked; check network |
| Sheet not updating | Check Apps Script execution transcript for errors |
License #
MIT License - See LICENSE file for details.