Skip to content

Getting Started

CodeScout works in two steps, and the first one does not need a server at all.

You need Flutter 3.38 or newer (Dart 3.10). If you are on an older one, flutter pub add will refuse to resolve rather than explain itself, so that is the first thing to check.

Terminal window
flutter pub add code_scout

One package and one call, and you have this on the device:

The Logs tab of the in-app CodeScout panel, showing filters and log rows

No server, no account, nothing uploaded. The dashboard comes later, and only if you want it.

CodeScout needs a BuildContext in order to place its floating button into your widget tree, so init() is called from a widget inside your MaterialApp, after the first frame has been drawn, rather than at the top of main(). The button goes into the nearest Overlay, and in a normal app that is the one MaterialApp builds, so a context taken from above it has nothing to insert into. Everything else in the SDK works without a context, but you will not get the in-app panel, and that panel is most of what makes the first ten minutes useful.

import 'package:code_scout/code_scout.dart';
import 'package:flutter/material.dart';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => const MaterialApp(home: Home());
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await CodeScout.instance.init(
// This is how the overlay finds a live context again after your app
// has navigated somewhere else.
freshContextFetcher: () => context,
configuration: CodeScoutConfiguration(
logging: LoggingBehavior(minimumLevel: LogLevel.all),
),
);
});
}
@override
Widget build(BuildContext context) =>
const Scaffold(body: Center(child: Text('Hello')));
}

That is a complete setup. You now have colour coded output in your console and a panel you can open on the phone itself, and nothing leaves the device. This is a genuine way to use Code Scout rather than a trial mode that expires.

Now write a few logs:

final scout = CodeScout.instance;
scout.d('Fetching user profile'); // debug
scout.i('User signed in', tags: {'auth'}); // info
scout.w('Cache miss', metadata: {'key': 'user_prefs'}); // warning
scout.e('Payment failed', error: e, stackTrace: st); // error

Tap the floating button and you will see three tabs.

Logs shows everything your app has written this launch. You can search it, switch individual levels on and off, filter by tag, and pause the list when it is moving faster than you can read it.

Network shows one row for every HTTP call, but only once you have completed step 5 below.

Errors shows errors and fatals on their own, counted, so that the same failure happening fifty times appears as a single line rather than filling the screen.

There are also two icons in the header of the panel. Info tells you whether anything is reaching a dashboard and what is currently being captured. Data appears once you have registered a database, and it lets you read that database on the device.

One thing is worth understanding about the panel. It reads the last 500 logs of the current launch from memory, so when you restart the app it starts empty again. That buffer is not the same thing as the SQLite store. SQLite exists to hold logs until they have been uploaded, and nothing on the device ever reads them back for you to browse.

Run the server, which is covered in Server Setup, create a project in it, and then add that project’s credentials to the configuration you already have:

configuration: CodeScoutConfiguration(
logging: LoggingBehavior(minimumLevel: LogLevel.all),
projectCredentials: ProjectCredentials(
link: 'https://scout.yourteam.dev/',
projectID: 'a3f2c7d1-4e88-4b21-9f60-1c2d3e4f9c41',
projectSecret: 'your-project-secret',
),
// Without this line nothing is ever uploaded. There is no default.
sync: LogSyncBehavior(syncInterval: Duration(seconds: 30)),
),

The link value has three rules, and the SDK throws an error when you construct ProjectCredentials rather than failing quietly on the first upload attempt. The link must begin with http:// or https://, it must contain a host, and it must end with a trailing slash.

So http://192.168.1.24:24275/ is accepted, while localhost:24275/ is rejected. The reason that second one fails is worth knowing, because it looks correct at a glance. Dart parses it as a URL whose scheme is localhost and whose host is empty, which means it is technically an absolute URI and completely unusable.

Open the panel and tap the info icon. The Info screen begins with a single line telling you either the name of the project you are connected to, or the specific reason that you are not connected. If you press Test connection, it runs four checks in order and stops at the first one that fails, so you can tell whether the problem is the address, the network or the secret.

Two mistakes account for most first attempts, and the Info screen names both of them.

The first is that localhost means the phone. On a real device, localhost refers to that device and not to your laptop, so you need to use your machine’s address on the local network instead.

The second is that Android blocks plain HTTP traffic by default. You can either use https, or allow cleartext for that particular host in your network security configuration while you are developing.

The core package deliberately has no HTTP dependency of its own, so network interception lives in a separate companion package. Install whichever one matches the client your app already uses:

Terminal window
flutter pub add code_scout_dio # if you use Dio
flutter pub add code_scout_http # if you use package:http
flutter pub add code_scout_talker # if you already log through Talker
import 'package:code_scout_dio/code_scout_dio.dart';
import 'package:dio/dio.dart';
final dio = Dio();
dio.interceptors.add(CodeScoutDioInterceptor());
import 'package:code_scout_http/code_scout_http.dart';
import 'package:http/http.dart' as http;
final client = CodeScoutHttpClient(client: http.Client());
final response = await client.get(Uri.parse('https://api.example.com/data'));

Every call writes one log when the request goes out, and at most one more: a response or an error, never both. They share a request id, which is how the panel and the dashboard know to show them as a single row rather than as two unrelated entries. A call whose answer was never recorded keeps only the first, and shows as pending.

Your own logs can carry that id too, which is how an error your model throws on a response body is shown beside the call that returned it. When the API changes under you covers it.

If your Network tab stays empty, the Info screen will tell you which of the three companion packages it can actually see. It is able to distinguish between a package that was never installed and one that is installed but has not captured anything yet, because each companion announces itself to the SDK at the moment you construct it.

Two things are not automatically limited to debug builds, and both will follow you into production if you do not deal with them.

The first is the floating button, which init() creates regardless of build mode. Either call CodeScout.instance.hideIcon(), or wrap the whole setup in a kDebugMode check.

The second is redaction, because nothing is redacted unless you ask for it. Out of the box CodeScout records the headers and bodies that your app sends, and that includes the Authorization header. This is deliberate, since the token is sometimes the very thing you are trying to debug, but it is not what you want when the app is in the hands of real users. RedactionBehavior.recommended() covers the usual suspects in a single line. Read Redaction and Privacy before you point this at anybody else.

Console printing is the one exception to all of this, because it already defaults to off in release builds.