| |

Dart 64 🎯 Advanced Logging, Diagnostics, and Trace Collection via Dart Zones

⏱️ Estimated read: 17 min | |

Dart zones are one of the most misunderstood features in the Dart language. They look simple on the surface—a way to run code in a special context—but they underpin almost every serious logging, error-handling, and observability pattern in production Dart and Flutter applications. This chapter focuses on the advanced use of zones for logging, diagnostics, and trace collection, building on the foundational zone mechanics covered earlier in this series. Where the basic zone chapter explained what zones are and how to fork them, this chapter answers a harder question: how do you actually use zones to build logging infrastructure that survives concurrent requests, propagates context through async boundaries, and captures errors that would otherwise vanish into the void?

The answer lies in understanding zones not as a curiosity but as the only reliable mechanism Dart provides for ambient context. Unlike dependency injection, which requires explicit passing through every constructor and function call, zone values travel invisibly with every asynchronous operation. When you fork a zone with a zoneValues map, every await, every Future.then, every Timer callback scheduled within that zone inherits those values. This is the foundation of request-scoped logging, where a requestId or userId can be attached to every log line without threading it through dozens of function signatures.

This chapter covers the architectural patterns for zone-based logging: how to build a request-scoped log context, how to intercept and enrich log records using ZoneSpecification, how to capture uncaught asynchronous errors that would otherwise crash silently, and how to structure trace collection so that a single request’s entire lifecycle—from HTTP handler to database query to response serialization—can be reconstructed from logs. We will examine the specific ZoneSpecification hooks that matter for diagnostics, the delegation pattern that makes zone composition work, and the practical trade-offs between zone-based context propagation and explicit dependency injection.

Key point: Zones are the only mechanism in Dart that automatically propagates ambient context through asynchronous boundaries, making them the correct foundation for request-scoped logging, structured diagnostics, and trace collection in concurrent applications.


Why zones matter for logging and diagnostics

The ambient context problem. In a concurrent server, a single isolate handles multiple HTTP requests simultaneously. Each request needs its own requestId, userId, and trace metadata attached to every log line it produces. Without zones, you either pass a logger object through every function call (invasive, error-prone) or use a global singleton (catastrophically broken under concurrency, because request B overwrites request A’s data the moment an await yields control) . Zones solve this by scoping values to the asynchronous execution context itself.

The error capture problem. Dart’s default behavior for uncaught asynchronous errors is to print to stderr and, in Flutter, potentially crash the app. But runZonedGuarded creates an error zone that intercepts every uncaught error—whether from a Timer, a Future, or I/O—before it reaches the root zone . This is the only reliable way to capture errors that occur outside a synchronous try-catch block.

The structured logging problem. Plain print() statements are unstructured strings. They cannot carry metadata, cannot be filtered by level, and cannot be routed to different sinks based on severity . Zones enable structured logging by allowing log records to be enriched with zone-local values at the moment of emission, without the logger needing to know where those values came from.

The trace correlation problem. When a request spans multiple services, you need a trace ID that follows the request from start to finish. Zones provide the mechanism to attach that trace ID once—at the entry point—and have it automatically present in every log line emitted anywhere in the call stack, including inside third-party libraries that know nothing about your logging system .

The delegation problem. When you fork a zone to add logging behavior, you must not break the parent zone’s behavior. ZoneDelegate exists precisely for this: it lets your custom zone handler forward operations to the parent zone after doing its own work, preserving the zone chain . Understanding delegation is essential for building zones that compose correctly rather than zones that accidentally disable error handling.


a. Request-scoped logging with Zone values

The core pattern for request-scoped logging is to wrap the handling of each request in a zone that carries the request’s identity. Every log emission inside that zone can then read the request identity from Zone.current without the emitting code knowing anything about where the value came from.

import 'dart:async';

// A simple zone key (any object works)
final _logContextKey = Object();

class LogContext {
  final String requestId;
  final String? userId;
  LogContext({required this.requestId, this.userId});
}

// Run code with a log context attached to the zone
Future<T> withLogContext<T>(
  LogContext context,
  Future<T> Function() body,
) {
  return runZoned(
    body,
    zoneValues: {_logContextKey: context},
  );
}

// Read the current context from anywhere
LogContext? get currentLogContext =>
    Zone.current[_logContextKey] as LogContext?;

The runZoned call creates a new zone whose zoneValues map contains the LogContext. Any asynchronous operation scheduled within body—including timers, microtasks, and I/O callbacks—will have Zone.current point to this forked zone, so currentLogContext returns the correct context .

void logInfo(String message) {
  final ctx = currentLogContext;
  final prefix = ctx != null ? '[${ctx.requestId}]' : '[no-context]';
  print('$prefix $message');
}

Future<void> handleRequest(String requestId) async {
  await withLogContext(LogContext(requestId: requestId), () async {
    logInfo('Request started');
    await Future.delayed(Duration(milliseconds: 10));
    logInfo('After delay — context survived'); // ctx is still available
    await processOrder();
  });
}

The second logInfo call demonstrates the critical property: the context survives await boundaries. This works because Dart associates each registered asynchronous callback with the zone in which it was scheduled . When Future.delayed schedules its completion callback, it registers that callback in the current zone—the forked zone—so when the callback runs, Zone.current is the forked zone, and the context is present.

This pattern scales to concurrent requests with no risk of cross-contamination. Two simultaneous requests each run in their own forked zone with their own zoneValues maps. When request A yields at an await, request B’s code runs in B’s zone, not A’s .


b. Intercepting print and structured output via ZoneSpecification

While zoneValues handle context propagation, ZoneSpecification handles behavior modification. The most immediately useful hook for logging is the print interceptor, which lets you redirect, enrich, or suppress all print() calls within a zone.

import 'dart:async';

class StructuredLogger {
  final List<Map<String, Object?>> records = [];

  void emit(String message, {Map<String, Object?>? fields}) {
    records.add({
      'timestamp': DateTime.now().toIso8601String(),
      'message': message,
      'fields': fields ?? {},
    });
  }
}

ZoneSpecification loggingSpecification(StructuredLogger logger) {
  return ZoneSpecification(
    print: (self, parent, zone, line) {
      // Enrich with any zone-local context
      final ctx = zone[#logContext] as Map<String, Object?>?;
      logger.emit(line, fields: ctx);
      // Do NOT delegate to parent — we're suppressing console output
    },
  );
}

The print interceptor receives self (the zone handling the call), parent (a ZoneDelegate for the parent zone), zone (the zone where print was called), and line (the string argument) . If you want to both log structurally and still see output on console during development, you can delegate:

print: (self, parent, zone, line) {
  logger.emit(line, fields: zone[#logContext] as Map<String, Object?>?);
  parent.print(zone, line); // forward to parent for console output
},

The delegation pattern is important: parent.print(zone, line) calls the parent zone’s print behavior, passing the originating zone as the first argument. The ZoneDelegate class exists specifically to make this forwarding efficient and correct . Without delegation, you replace the entire print behavior for the subtree; with delegation, you augment it.

The same ZoneSpecification mechanism can intercept scheduleMicrotask, createTimer, and registerCallback, allowing you to instrument every asynchronous operation that occurs within a zone. For diagnostics, this is powerful: you can log every timer scheduled, every microtask registered, with timing information, without modifying any application code .


c. Capturing uncaught errors and trace collection

runZonedGuarded is a shorthand for creating a zone with an error handler that catches everything the zone’s code throws asynchronously . In production diagnostics, this is where trace collection begins.

import 'dart:async';

class TraceCollector {
  final List<TraceEntry> entries = [];

  void record(Object error, StackTrace stackTrace) {
    entries.add(TraceEntry(
      error: error,
      stackTrace: stackTrace,
      timestamp: DateTime.now(),
      context: Zone.current[#logContext],
    ));
  }
}

void main() {
  final collector = TraceCollector();

  runZonedGuarded(
    () {
      // All application code runs here
      scheduleMicrotask(() {
        throw StateError('Microtask failure');
      });

      Timer(Duration(milliseconds: 100), () {
        throw ArgumentError('Timer failure');
      });

      Future.delayed(Duration(milliseconds: 200), () {
        // Even after awaits, errors are captured
        throw Exception('Async failure');
      });
    },
    (error, stackTrace) {
      collector.record(error, stackTrace);
    },
  );
}

The runZonedGuarded signature takes a body and an error handler. The handler receives the error and stack trace for any uncaught asynchronous error from within the zone . Crucially, this captures errors from Timer callbacks, microtasks, and Future completions that would otherwise be unhandled.

For trace collection specifically, you want the stack trace to include asynchronous call chains. Dart’s default stack traces for async errors often show only the immediate callback, not the full path of how execution reached that point. The snug_logger package demonstrates one approach: wrapping execution in a zone with a #stackTrace zone value and using a custom handleUncaughtError that captures the full chain . The key insight is that zone interception happens at the point of error, when the zone chain is still intact, allowing reconstruction of the async call path.

ZoneSpecification traceSpecification() {
  return ZoneSpecification(
    handleUncaughtError: (self, parent, zone, error, stackTrace) {
      // Record with full zone context before delegating
      final traceId = zone[#traceId];
      print('Trace $traceId: $error');
      // Delegate so parent zones (and FlutterError) also see it
      parent.handleUncaughtError(zone, error, stackTrace);
    },
  );
}

The handleUncaughtError interceptor is called for errors that reach the zone’s error boundary. By delegating to parent, you ensure that if a parent zone (like the root zone or Flutter’s error handler) also has error handling, it still receives the error . This composition is what makes zone-based error handling practical: you can add diagnostic capture without breaking existing error reporting.


Complete Example Session

The following session builds a complete zone-based diagnostics system for a simulated HTTP server. It demonstrates request-scoped logging, print interception, error capture, and trace correlation.

// ============================================
// PART 1: ZONE KEYS AND CONTEXT TYPES
// ============================================
import 'dart:async';
import 'dart:math';

final _requestContextKey = Object();
final _traceIdKey = Object();

class RequestContext {
  final String requestId;
  final String userId;
  final DateTime startTime;

  RequestContext({
    required this.requestId,
    required this.userId,
    DateTime? startTime,
  }) : startTime = startTime ?? DateTime.now();
}

// ============================================
// PART 2: THE STRUCTURED LOGGER
// ============================================

class DiagnosticLogger {
  final List<String> output = [];
  bool _silent = false;

  void emit(String level, String message, {Map<String, Object?>? fields}) {
    if (_silent) return;

    final ctx = Zone.current[_requestContextKey] as RequestContext?;
    final traceId = Zone.current[_traceIdKey] as String?;

    final parts = <String>[
      DateTime.now().toIso8601String(),
      level.padRight(5),
      if (ctx != null) '[${ctx.requestId}]',
      if (traceId != null) '[$traceId]',
      message,
      if (fields != null && fields.isNotEmpty) '$fields',
    ];

    output.add(parts.where((p) => p.isNotEmpty).join(' '));
  }

  void info(String message, {Map<String, Object?>? fields}) =>
      emit('INFO', message, fields: fields);

  void error(String message, {Map<String, Object?>? fields}) =>
      emit('ERROR', message, fields: fields);

  void suppress() => _silent = true;
  void unsuppress() => _silent = false;
}

final logger = DiagnosticLogger();

// ============================================
// PART 3: REQUEST ZONE WRAPPER
// ============================================

Future<T> runWithRequest<T>(
  RequestContext ctx,
  Future<T> Function() body,
) {
  return runZoned(
    body,
    zoneValues: {
      _requestContextKey: ctx,
      _traceIdKey: 'trace-${ctx.requestId}',
    },
    zoneSpecification: ZoneSpecification(
      print: (self, parent, zone, line) {
        // Intercept print and route to structured logger
        final ctx = zone[_requestContextKey] as RequestContext?;
        logger.emit('PRINT', line, fields: {'requestId': ctx?.requestId});
        // Do not delegate — we want structured output only
      },
    ),
  );
}

// ============================================
// PART 4: ERROR CAPTURE ZONE
// ============================================

final List<Map<String, Object?>> capturedErrors = [];

Future<void> runApplication() async {
  return runZonedGuarded(
    () async {
      logger.info('Application starting');

      // Simulate handling three concurrent requests
      await Future.wait([
        simulateRequest('req-001', 'user-A'),
        simulateRequest('req-002', 'user-B'),
        simulateRequest('req-003', 'user-A'),
      ]);

      logger.info('Application shutting down');
    },
    (error, stackTrace) {
      capturedErrors.add({
        'error': error.toString(),
        'stackTrace': stackTrace.toString().split('\n').take(3).join('\n'),
        'timestamp': DateTime.now().toIso8601String(),
      });
      logger.error('Uncaught error: $error');
    },
  );
}

// ============================================
// PART 5: SIMULATED REQUEST HANDLER
// ============================================

Future<void> simulateRequest(String requestId, String userId) async {
  final ctx = RequestContext(requestId: requestId, userId: userId);

  await runWithRequest(ctx, () async {
    logger.info('Request received', fields: {'userId': userId});

    // Simulate async database lookup
    await Future.delayed(Duration(milliseconds: 10 + Random().nextInt(20)));
    logger.info('Database query complete');

    // Simulate business logic
    await processBusinessLogic(requestId);

    logger.info('Request completed',
        fields: {'elapsed': '${DateTime.now().difference(ctx.startTime).inMilliseconds}ms'});
  });
}

Future<void> processBusinessLogic(String requestId) async {
  // Sometimes throw to demonstrate error capture
  if (requestId == 'req-003') {
    throw StateError('Simulated failure for $requestId');
  }

  await Future.delayed(Duration(milliseconds: 5));
  logger.info('Business logic processed');
}

// ============================================
// PART 6: MAIN ENTRY POINT
// ============================================

Future<void> main() async {
  await runApplication();

  print('\n=== CAPTURED LOG OUTPUT ===');
  for (final line in logger.output) {
    print(line);
  }

  print('\n=== CAPTURED ERRORS ===');
  for (final err in capturedErrors) {
    print('Error: ${err['error']}');
    print('Trace: ${err['stackTrace']}');
  }
}

The complete example demonstrates the full pattern: request contexts are created at the handler level and propagated via zones; log output is intercepted and enriched with context; errors from concurrent requests are captured with their zone context intact; and the trace ID is automatically present in every log line without explicit passing. The ten parts cover zone keys, the logger, the request wrapper with print interception, error capture, concurrent request simulation, and the entry point. What the session proves is that zones provide the only viable mechanism for this kind of ambient diagnostics in Dart—dependency injection would require threading the logger and context through every function call, and globals would break under concurrency.


Quick Reference

Zone Value Operations

OperationCodeEffect
Read valueZone.current[key]Returns value or null
Create zonerunZoned(body, zoneValues: {key: value})New zone with values
Fork explicitlyZone.current.fork(zoneValues: {key: value})Child zone
Run in zonezone.run(body)Executes in existing zone

ZoneSpecification Interceptors

InterceptorSignatureUse Case
print(self, parent, zone, line)Structured logging
handleUncaughtError(self, parent, zone, error, stackTrace)Error capture
scheduleMicrotask(self, parent, zone, callback)Timing instrumentation
createTimer(self, parent, zone, duration, callback)Async operation tracing

Error Zone Behavior

ScenariorunZonedrunZonedGuarded
Sync throw in bodyPropagates to callerPropagates to caller
Async uncaught errorCrashesHandler receives error
Timer callback throwsCrashesHandler receives error
Future completion throwsCrashesHandler receives error

Context Propagation Properties

PropertyBehavior
Across awaitContext survives
Across TimerContext survives
Across StreamContext survives
Across isolateContext does not survive

Delegation Patterns

PatternCodeEffect
Suppress// no parent callReplaces behavior
Augmentparent.method(zone, args)Adds to behavior
Forward unchangedparent.method(zone, args) after loggingInstrumentation only

Best Practices

✅ Do This:

runZoned(() => handleRequest(req), zoneValues: {#requestId: req.id}); // ✅
Zone.current.fork(zoneValues: {#traceId: id}).run(body);               // ✅
print: (self, parent, zone, line) { logger.emit(line); };              // ✅
parent.handleUncaughtError(zone, error, stackTrace);                   // ✅
final ctx = Zone.current[#requestContext] as RequestContext?;          // ✅
scheduleMicrotask(() => logInfo('deferred'));                          // ✅
runZonedGuarded(main, (e, st) => report(e, st));                       // ✅

❌ Don’t Do This:

globalRequestId = req.id; // shared mutable global — breaks concurrency ❌
Zone.current.fork(zoneValues: {#requestId: req.id}); // not run — forgotten ❌
print: (self, parent, zone, line) { print(line); }; // infinite recursion ❌
// no parent delegation — breaks Flutter error reporting ❌
final ctx = Zone.current[#requestContext] as RequestContext; // null crash ❌
Future.microtask(() => logInfo('deferred')); // still in zone, but explicit ❌
main(); // no error zone — uncaught errors crash ❌

Common Pitfalls

PitfallWhy It HappensFix
Zone value null at runtimeReading key not set in current zoneAlways null-check: Zone.current[key] as T?
Context lost after awaitCallback scheduled in different zoneEnsure entry point uses runZoned
Infinite recursion on printInterceptor calls print() againUse parent.print(zone, line) or logger directly
Error handler not calledError thrown synchronously outside zoneSync errors need try-catch; zones only catch async
Delegation forgottenReplacing behavior instead of augmentingCall parent.method(zone, args) unless suppressing
Zone values mutableUsing mutable object as zone valueZone values should be immutable; create new for changes
Memory leak from zonesHolding references to zones after completionZones are GC’d when no longer referenced; avoid static refs
Isolate context not propagatedZone values don’t cross isolate boundariesPass context explicitly in isolate message

Real-World Examples

1. HTTP Middleware Request Context

server.listen((req) {
  runZoned(() => route(req), zoneValues: {#req: req});
});

2. Riverpod Provider Logging

ref.logger.info('Initializing'); // Context auto-detected via Zone

3. Flutter Error Reporting

runZonedGuarded(() => runApp(MyApp()), (e, st) => reportError(e, st));

4. Trace ID Propagation

Log.scope({'request_id': id}, () => service.handle());

5. Suppressing Print in Tests

runZoned(() => testBody(), zoneSpecification: ZoneSpecification(print: (s,p,z,l){}));

6. Capturing Stack Chains Async

StackTraceFormatter.capture(() => asyncChain(), onError: (e, c) => log(e, c));

7. Delegated Error Handling

handleUncaughtError: (s,p,z,e,st) { capture(e); p.handleUncaughtError(z,e,st); }

8. Named Logger Context

Log.named('UserService').info('Finding user', {'id': userId});

9. State Change Detection

ref.logger.info('State changed: $old → $new'); // auto context

10. Isolate Error Forwarding

Isolate.current.addErrorListener(port.sendPort); // forward to zone

Visual

The Zone Hierarchy and Error Boundaries

┌──────────────────────────────────────────────┐
│  ROOT ZONE (Zone.root)                       │
│  Default error handling: print + crash       │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │  APPLICATION ZONE (runZonedGuarded)    │  │
│  │  Error boundary: handler captures all  │  │
│  │                                        │  │
│  │  ┌──────────────────────────────────┐  │  │
│  │  │  REQUEST ZONE (runZoned)         │  │  │
│  │  │  zoneValues: {requestId, userId} │  │  │
│  │  │  print interceptor active        │  │  │
│  │  │                                  │  │  │
│  │  │  await db.query() ──┐            │  │  │
│  │  │  await api.call() ──┼─ callbacks │  │  │
│  │  │  Timer(…, log) ─────┘  inherit  │  │  │
│  │  │                         this zone│  │  │
│  │  └──────────────────────────────────┘  │  │
│  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

Context Propagation Through Async Boundaries

┌──────────────────────────────────────────────┐
│  ZONE WITH zoneValues: {requestId: "abc"}    │
│                                              │
│  handleRequest()                             │
│    │                                         │
│    ├── log("start")        → reads "abc"    │
│    │                                         │
│    ├── await db.query()                      │
│    │     │                                   │
│    │     └── callback registered in zone    │
│    │           │                             │
│    │           └── log("db done") → "abc"   │
│    │                                         │
│    ├── Timer(…, () { log("timer") })        │
│    │     │                                   │
│    │     └── scheduled in zone → "abc"      │
│    │                                         │
│    └── log("end")          → reads "abc"    │
│                                              │
│  All emissions see requestId without         │
│  explicit passing.                           │
└──────────────────────────────────────────────┘

Error Capture Flow

┌──────────────────────────────────────────────┐
│  ASYNC ERROR OCCURS                          │
│                                              │
│  Future.delayed(…, () => throw Error())     │
│         │                                    │
│         ▼                                    │
│  Error propagates through zone chain         │
│         │                                    │
│         ▼                                    │
│  REQUEST ZONE handleUncaughtError?           │
│         │                                    │
│    ┌────┴────┐                               │
│    │         │                               │
│  custom    no custom                         │
│  handler   handler                           │
│    │         │                               │
│    ▼         ▼                               │
│  capture   delegate to parent                │
│  + log     │                                 │
│    │         │                               │
│    └────┬────┘                               │
│         ▼                                    │
│  APP ZONE handleUncaughtError                │
│         │                                    │
│         └── reports, does not crash          │
└──────────────────────────────────────────────┘

ZoneSpecification Interception Points

┌──────────────────────────────────────────────┐
│  ZONE SPECIFICATION HOOKS                    │
│                                              │
│  Code calls:       Interceptor:              │
│  ┌─────────────┐   ┌──────────────────────┐  │
│  │ print(x)    │──▶│ print(self,p,z,line) │  │
│  └─────────────┘   └──────────────────────┘  │
│                                              │
│  ┌─────────────┐   ┌──────────────────────┐  │
│  │ Timer(…)    │──▶│ createTimer(self,…)  │  │
│  └─────────────┘   └──────────────────────┘  │
│                                              │
│  ┌─────────────┐   ┌──────────────────────┐  │
│  │ throw async │──▶│ handleUncaughtError  │  │
│  └─────────────┘   └──────────────────────┘  │
│                                              │
│  ┌─────────────┐   ┌──────────────────────┐  │
│  │ microtask(f)│──▶│ scheduleMicrotask    │  │
│  └─────────────┘   └──────────────────────┘  │
│                                              │
│  Each hook can: log, enrich, delegate,       │
│  suppress, or replace.                       │
└──────────────────────────────────────────────┘

Summary

ItemValue
ZoneEnvironment stable across async calls
zoneValuesImmutable map of ambient context
runZonedCreates zone with values and specification
runZonedGuardedrunZoned + error handler shortcut
ZoneSpecificationOverrides print, error, scheduling behavior
ZoneDelegateForwards operations to parent zone
handleUncaughtErrorError interceptor for zone boundary
print interceptorCaptures all print calls in zone
Context propagationAutomatic through await, Timer, Stream
Isolate boundaryZone values do not cross
DelegationRequired for composition

Key takeaways:

  • Zones propagate context automatically. Any value in zoneValues is available via Zone.current[key] in every callback scheduled within that zone, including after await boundaries .
  • runZonedGuarded is the error capture mechanism. It intercepts all uncaught asynchronous errors in its subtree, including those from timers, microtasks, and I/O .
  • ZoneSpecification modifies behavior without subclassing. The print interceptor is the most immediately useful hook for structured logging .
  • Delegation preserves zone composition. Always call parent.method(zone, args) unless you intend to suppress the parent’s behavior .
  • Request-scoped logging solves the concurrency problem. Global mutable state breaks under concurrent requests; zones provide isolated per-request context .
  • Zone values are immutable by convention. Create new zones with new values rather than mutating context objects.
  • Context does not cross isolate boundaries. Pass context explicitly in isolate messages.
  • package:logging and zone-based loggers complement each other. The logging package handles record routing; zones handle context enrichment .
  • The snug_logger pattern captures full async stack chains. Wrapping execution in a zone with handleUncaughtError interception preserves the call path .
  • Production diagnostics requires both context and error capture. Context alone tells you where a log came from; error capture tells you what went wrong.

Remember: Zones are not a feature you use occasionally—they are the ambient execution context that every asynchronous operation in Dart inherits. When you build logging infrastructure, you are either working with zones explicitly or fighting against them implicitly. The correct architecture places request context in zoneValues at the entry point, intercepts print for structured emission, captures errors via runZonedGuarded, and always delegates to parent zones so that composition works. This gives you request-scoped logs that survive await, error traces that include their origin, and a diagnostics layer that requires zero modification to the application code that emits the logs.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!