telemetry_flight_recorder 0.1.0
telemetry_flight_recorder: ^0.1.0 copied to clipboard
High-reliability, zero-impact silent telemetry and flight recorder for Flutter. Captures console breadcrumbs, storage snapshots, and network traces with zero UI thread overhead.
telemetry_flight_recorder #
High-reliability, zero-impact silent flight recorder and runtime telemetry for Flutter applications. Built for production scale (1M+ active users) with zero frame drops, sub-microsecond log overhead, and automatic Gzip wire compression.
1. Core Architecture & Guiding Principles #
- Strict Fire-and-Forget: Telemetry dispatches must never block, await, or disrupt host application flows. If network fails or hardware hangs, errors are silently absorbed.
- Delta Log Ingestion: Tracks
last_sent_epoch_ms. Each flush only transmits new logs generated since the last push, avoiding redundant data transfer. - In-Memory Gzip Compression: Compresses wire payloads by >85% (average 200 logs shrink from ~22 KB to ~2 KB).
- Hardware Keystore Isolation: Wraps
FlutterSecureStoragereads in an 800ms race timeout to prevent deadlocks on older Android devices. - PII & Secret Sanitization: Built-in regex redacts tokens (
tf_access_token,refresh_token), passwords, PINs, OTPs, and PII before serialization. - Approach C Kill-Switch: Backend can globally bypass all Redis processing with a 1-line check (
TELEMETRY_ENABLED !== 'false'), responding in <1ms without requiring app releases.
2. Performance Benchmarks #
Measured on physical ARM64 runtime:
- Single Log Ingestion: ~
1.3microseconds per entry ($0.0013\text{ ms}$). - Gzip Compression (200 Logs): ~
1.7milliseconds. - Idle Memory Footprint: $< 50\text{ KB}$ (instant memory drain upon push).
- UI Thread Impact: 0 frame drops (60/120 fps preserved).
3. Flutter Integration #
pubspec.yaml #
dependencies:
flutter_flight_recorder:
path: ../flutter_flight_recorder
lib/main.dart #
import 'package:flutter_flight_recorder/flutter_flight_recorder.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Crashlytics first so FlightRecorder can chain onto it
await Firebase.initializeApp();
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
// Initialize FlightRecorder
await FlightRecorder.init(
FRConfig(
redisIngestUrl: "https://api.altdrx.com/api/v1/telemetry/report",
flushInterval: const Duration(minutes: 3),
appVersion: "15.2.215",
runtimeAttributesProvider: () => {
'session.tokenType': SessionHandler.instance.type.name,
'session.expiryTimestamp': SessionHandler.instance.expiryTimestamp,
'user.user_id': User.user_id,
},
),
);
runApp(const MyApp());
}
Network Logging #
Option A: Automatic Wrapper
final client = FlightRecorder.wrapClient(http.Client());
Option B: In Custom HTTP Interceptor
FlightRecorder.logHttp(
method: 'POST',
url: 'https://api.altdrx.com/v1/kyc/verify',
statusCode: response.statusCode,
durationMs: durationMs,
);
User Identification #
// On sign in
FlightRecorder.setUserId("usr_98124");
// On sign out
FlightRecorder.clearUserId();
4. Backend Ingestion Gateway (ingest_server.js) #
High-throughput Node.js / Express gateway handling Gzip payloads and Approach C kill-switch:
const express = require('express');
const zlib = require('zlib');
const Redis = require('ioredis');
const app = express();
const redis = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
// Raw body parser for Gzip payloads
app.use(express.raw({ type: '*/*', limit: '1mb' }));
app.post('/api/v1/telemetry/report', async (req, res) => {
// Approach C Kill-Switch: Immediate 200 OK without Redis push
if (process.env.TELEMETRY_ENABLED === 'false') {
return res.status(200).json({ status: 'bypassed' });
}
try {
let payloadString;
// Handle Gzip decompression if encoded
if (req.headers['content-encoding'] === 'gzip') {
payloadString = zlib.gunzipSync(req.body).toString('utf-8');
} else {
payloadString = req.body.toString('utf-8');
}
const doc = JSON.parse(payloadString);
doc.receivedAt = new Date().toISOString();
// Push into Redis queue with 5-minute TTL safety net
await redis.lpush('telemetry:log_queue', JSON.stringify(doc));
return res.status(200).json({ status: 'queued' });
} catch (err) {
// Return 200 to prevent mobile clients from retrying
return res.status(200).json({ status: 'swallowed_parse_error' });
}
});
app.listen(3000, () => console.log('Telemetry Ingestion Gateway running on :3000'));
5. Background Redis-to-MongoDB Worker (worker.js) #
const Redis = require('ioredis');
const { MongoClient } = require('mongodb');
const redis = new Redis(process.env.REDIS_URL || 'redis://127.0.0.1:6379');
const mongo = new MongoClient(process.env.MONGO_URI || 'mongodb://127.0.0.1:27017');
async function startWorker() {
await mongo.connect();
const db = mongo.db('altdrx_observability');
const collection = db.collection('device_log_sets');
// Automatic 5-day TTL index
await collection.createIndex({ "receivedAt": 1 }, { expireAfterSeconds: 432000 });
await collection.createIndex({ "investorId": 1, "sentAtEpoch": 1 });
await collection.createIndex({ "sessionId": 1 });
console.log('Telemetry Worker active. Draining Redis queue into MongoDB...');
while (true) {
try {
const pipeline = redis.pipeline();
for (let i = 0; i < 100; i++) {
pipeline.rpop('telemetry:log_queue');
}
const results = await pipeline.exec();
const batch = results
.map(([err, val]) => val)
.filter(Boolean)
.map(jsonStr => {
const doc = JSON.parse(jsonStr);
doc.receivedAt = new Date(doc.receivedAt);
return doc;
});
if (batch.length > 0) {
await collection.insertMany(batch);
console.log(`[Worker] Flushed ${batch.length} log sets to MongoDB.`);
} else {
await new Promise(r => setTimeout(r, 2000));
}
} catch (e) {
console.error('Worker error:', e);
await new Promise(r => setTimeout(r, 3000));
}
}
}
startWorker();