dartpub.dev
plugins / telling_kit
tk

telling_kit

v1.0.1 MIT

Crash reporting, error tracking and analytics for DartNative apps.

Telling is the DartNative edition of the telling_logger Flutter package: crash reporting, error tracking and analytics with the same public API, the same wire format and the same dashboard — so code written against the Flutter SDK ports by changing one import. Pure Dart, no Flutter and no method channels. Crash capture works on any Dart runtime: an Isolate.addErrorListener over a RawReceivePort, which also catches a synchronous throw during main(), plus a runZonedGuarded zone around the app body. Crashes are fingerprinted by type, message and top stack frame, so one crash is one report. Layout failures are classified as warnings so a dashboard keeps separating layout noise from real faults.

by ThatSaxyDev/telling_kit · updated 2 days ago
Install
Free
pubspec.yaml
dependencies:
  telling_kit:
    hosted: https://dartpub.dev
    version: ^1.0.1
Weekly installs
54
Active apps
1
Rating
0.0 · 0
Open issues
0

telling_kit

Telling for DartNative — crash reporting, error tracking and analytics, with the telling_logger API. Code written against the Flutter package ports by changing one import.

// import 'package:telling_logger/telling_logger.dart';
import 'package:telling_kit/telling_kit.dart';

telling_logger declares flutter: {sdk: flutter} and depends on Flutter plugins, so it cannot resolve in a DartNative app — DartNative omits flutter: sdk: from its own pubspec to keep consumer lockfiles Flutter-free, and its Zero engine calls native code over FFI instead of platform channels. telling_kit keeps the API and the ingestion contract and replaces the five Flutter-bound pieces:

telling_logger telling_kit
shared_preferences dartnative_shared_preferences
package_info_plus dartnative_system's PackageInfo
device_info_plus libc via dart:ffi — see No native code
FlutterError.onError, PlatformDispatcher.instance.onError Dart zones + an isolate error port
NavigatorObserver screen tracking explicit Telling.instance.screenView(...)
kDebugMode, kIsWeb bool.fromEnvironment('dart.vm.product')

Setup

dependencies:
  dartnative: ^1.0.0
  dartnative_shared_preferences: ^1.0.0
  dartnative_system: ^1.0.0
  telling_kit:
    hosted: https://dartpub.dev
    version: ^1.0.1
dn pub get

dn pub get resolves the package and the two SDK-managed plugins, then generates the plugin registrant. Declare dartnative_shared_preferences and dartnative_system directly in the app, as above: the registry source archive omits SDK-managed dependencies from the pubspec supplied to Dart pub. There is nothing to link: no .xcframework, no .aar, no podspec, no gradle module.

Quickstart

import 'package:dartnative/dartnative.dart';
import 'dartnative_plugin_registrant.dart';
import 'package:telling_kit/telling_kit.dart';

void main() {
  DartNativePluginRegistrant.registerAll();

  // Arm the isolate error listener before anything else can throw.
  Telling.instance.armCrashCapture();

  Telling.instance.run(() => runApp(const MyApp()));
}

Future<void> startTelling(String apiKey) async {
  await Telling.instance.init(apiKey);
  Telling.instance.enableCrashReporting();
}

Two rules, both load-bearing:

  1. run(...) must wrap the app body. It installs the zone that catches the async gaps the isolate reports without user context. It is a no-op passthrough when crash reporting was never enabled, so calling it unconditionally is safe. An app that owns its own zone should forward into TellingCrashReporter.handleZoneError rather than nesting a second zone.
  2. Identify the user before tracking a funnel. setUser() ends the current session, so calling it mid-funnel makes the backend see the steps as belonging to different people.

init is safe to call with no key at all — every Telling call then degrades to a no-op, which is what makes this package testable in CI and debug builds without a key.

Keeping the key out of source

Read it from a build-time define rather than committing it:

const apiKey = String.fromEnvironment('TELLING_API_KEY');

What you get

  • Analytics — event, trackFunnel, screenView, and user identity (setUser, clearUser, setUserName, setUserProperty, setUserProperties).
  • Logs — log at six levels (trace … fatal), typed general / analytics / crash / performance.
  • Errors — captureException for handled failures, plus automatic capture of uncaught ones. TellingTryCatch (tryRun, tryRunVoid, tryRunSync) wraps calls that should report and carry on.
  • Version checks — checkVersion, snoozeUpdate, acceptUpdate.
  • Transport — gzip past 1 KB, offline persistence through dartnative_shared_preferences, replay with exponential backoff (5s → 80s). A burst of handled failures costs one request, not one per event.
  • Rate limiting — 10 events/second, a 5s dedupe window and a 5s crash throttle, so a hot loop cannot flood ingestion.

No native code

Device metadata does not need a plugin either. Both platforms expose what device_info_plus reports through libc, which DynamicLibrary.process() resolves directly:

  • Apple: sysctlbyname("hw.machine") → iPhone17,1, the same string IosInfo.utsname.machine returns, so dashboards group identically.
  • Android: __system_property_get("ro.product.model") and ro.product.manufacturer, joined the way AndroidInfo formats them.

Every lookup degrades to null on failure. Observability must never be the thing that takes an app down.

Crash capture

This is the one place the port is a genuine rewrite rather than a swap. enableCrashReporting() arms both paths that work on any Dart runtime:

  1. Isolate.addErrorListener over a RawReceivePort — sees anything that escaped every try/catch and every zone handler, including a synchronous throw during main(). Strictly stronger than Flutter's PlatformDispatcher.instance.onError, which only sees errors inside a zone. Call armCrashCapture() before init() so early throws are buffered rather than lost.
  2. runZonedGuarded, installed by Telling.instance.run(...) around the app body — catches the async gaps (Future with no errorHandler, a bare await that throws) that the isolate reports asynchronously with no user context.

One fatal error is commonly reported by both, because an error propagating out of runZonedGuarded still reaches the isolate. Crashes are fingerprinted (type + message + top stack frame) and repeats inside a 2-second window are collapsed, so a crash is one report.

Layout failures (RenderFlex overflowed, constraints, disposed, …) are classified as warnings, everything else as fatal — the same substring heuristic the Flutter version used, so an existing dashboard keeps separating layout noise from real crashes.

Screen tracking

screenTracker and goRouterScreenTracker are gone. DartNative's NavigatorObserver is an empty class and App.navigatorObserver is never invoked, so there is no hook to subclass — the Flutter-compatible name exists so imported code compiles, not because anything calls it.

Telling.instance.screenView('race_detail');

Re-entering the current screen is ignored, and time-on-previous-screen is still logged automatically.

Example

example/ is a runnable DartNative app wired to every feature — events, funnels, logs at each level, a handled exception, and a real uncaught crash.

cd example
dn pub get
dn run --dart-define=TELLING_API_KEY=your_key

Testing

dn test

debugUseTransport / debugResetTransport swap in a fake transport, so tests can assert on request volume and payloads without a network.

Relationship to telling_logger

Same API, same wire format, same dashboard. The Flutter package remains the right choice for a Flutter app; this one exists because a DartNative app cannot load it.

Files carried across unchanged — lib/src/models/, lib/src/rate_limiter.dart, lib/src/utils/stack_trace_parser.dart, lib/src/telling_try_catch.dart — are kept in the old if (field != null) 'field': field form rather than the newer ?'field': field marker, deliberately, so they stay diffable against the Flutter original when re-syncing. Only stack_trace_parser.dart's column entry must keep the old form: column.toString() is never null, so the null-aware marker would be a type error there.

License

MIT — see LICENSE.

Version and build checks

From version 1.0.1, checkVersion() automatically sends the installed build number alongside the version to Telling's project API. Configure optional iOS and Android minimum builds in your project's version configuration in the Telling app or website. Version and build minimums are independent; falling below either triggers an update. Use positive integer builds that increase across releases. Clear a minimum build to disable it. Missing or nonnumeric installed builds keep the version-only behavior.

final result = await Telling.instance.checkVersion();
if (result.requiresUpdate && !result.isRequired && result.minVersion != null) {
  // When the user chooses Later:
  await Telling.instance.snoozeUpdate(
    days: 3,
    minVersion: result.minVersion!,
    target: result.snoozeTarget,
  );
}
// Before opening the update URL after the user accepts:
// await Telling.instance.acceptUpdate(
//   minVersion: result.minVersion, target: result.snoozeTarget,
// );

Results include minBuild, target, and decisionSource. snoozeTarget falls back to minVersion against older servers. A new build target prompts again even during an earlier optional-update snooze. Mandatory updates bypass snoozes. Checks fail open on HTTP, parsing or timeout errors. Old method calls remain source-compatible; pass the target to snooze build-based updates. No DartNative native binding changes are needed.