Skip to main content
Obfuscation makes your code harder to reverse engineer, but it also makes debugging more challenging. Refractor generates a symbol map that maps obfuscated names back to their originals, allowing you to deobfuscate stack traces and debug issues in production.

Symbol Maps

The SymbolTable class tracks all identifier renames during obfuscation:
class SymbolTable {
  // obfuscated -> original
  final Map<String, String> _map = {};

  void record(String original, String obfuscated) {
    _map[obfuscated] = original;
  }

  String? original(String obfuscated) => _map[obfuscated];
}
Source: lib/src/engine/symbol_table.dart:10-20

Enabling Symbol Map Output

Configure the symbol map path in refractor.yaml:
refractor:
  symbol_map: symbol_map.json
  exclude:
    - "**/*.g.dart"
  verbose: false

passes:
  rename:
    preserve_main: true
The symbol map is written after every build:
final result = BuildResult(
  outputPath: request.output,
  symbolTable: symbolTable,
  passesRun: enabledPasses.map((p) => p.name).toList(),
);
Source: lib/src/engine/engine.dart:78-82

Symbol Map Format

The symbol map is exported as a JSON file mapping obfuscated names to originals:
{
  "a": "UserRepository",
  "b": "AuthService",
  "c": "DatabaseConnection",
  "d": "fetchUserById",
  "e": "validateCredentials",
  "f": "_privateHelper"
}
The format is:
Map<String, String> toJson() => Map.unmodifiable(_map);
String toJsonString() => const JsonEncoder.withIndent('  ').convert(_map);
Source: lib/src/engine/symbol_table.dart:37-40

Deobfuscating Stack Traces

1

Capture Obfuscated Stack Trace

When your obfuscated application crashes, you’ll see obfuscated names:
Unhandled exception:
#0      a.d (package:myapp/a.dart:42)
#1      b.e (package:myapp/b.dart:17)
#2      c.f (package:myapp/c.dart:89)
2

Load the Symbol Map

Read the generated symbol_map.json:
import 'dart:convert';
import 'dart:io';

final symbolMap = json.decode(
  File('symbol_map.json').readAsStringSync(),
) as Map<String, dynamic>;
3

Replace Obfuscated Names

Map each obfuscated identifier back to its original:
String deobfuscate(String obfuscatedTrace, Map<String, String> symbolMap) {
  var result = obfuscatedTrace;
  symbolMap.forEach((obfuscated, original) {
    result = result.replaceAll(obfuscated, original);
  });
  return result;
}
4

Review Deobfuscated Trace

The readable stack trace shows original names:
Unhandled exception:
#0      UserRepository.fetchUserById (package:myapp/repositories/user.dart:42)
#1      AuthService.validateCredentials (package:myapp/services/auth.dart:17)
#2      DatabaseConnection._executeQuery (package:myapp/db/connection.dart:89)

Symbol Map Lookup Methods

The SymbolTable provides bidirectional lookup:

Original from Obfuscated

String? original(String obfuscated) => _map[obfuscated];
Usage:
final original = symbolTable.original('a'); // "UserRepository"

Obfuscated from Original (Reverse Lookup)

String? obfuscated(String original) {
  for (final entry in _map.entries) {
    if (entry.value == original) return entry.key;
  }
  return null;
}
Usage:
final obfuscated = symbolTable.obfuscated('UserRepository'); // "a"
Source: lib/src/engine/symbol_table.dart:19-28

Development vs Production Builds

For development builds, consider disabling obfuscation entirely or using minimal passes:
# refractor.dev.yaml
refractor:
  symbol_map: dev_symbols.json
  verbose: true

passes:
  rename: false
  string_encrypt: false
  dead_code: false
Build with:
refractor build --config refractor.dev.yaml
With all passes disabled, the output is functionally equivalent to a standard dart compile, making debugging trivial.

Securing Symbol Maps

Store Separately

Keep symbol maps in a secure location separate from deployed artifacts.

Version Control

Git-ignore symbol maps or store in a private repository with restricted access.

Build Metadata

Tag symbol maps with build version, commit hash, and timestamp for traceability.

Access Control

Limit symbol map access to authorized developers and incident response teams.
# Ignore symbol maps to prevent accidental commits
symbol_map.json
build/symbol_map.json
*.symbols.json

# Ignore build artifacts
build/
*.dill
*.aot
*.jit

Best Practices

Always use preserve_main: true in the rename pass to keep your entrypoint discoverable:
passes:
  rename:
    preserve_main: true
This prevents the main() function from being renamed, allowing the Dart VM to find it.
Store symbol maps alongside release artifacts:
releases/
  v1.0.0/
    app.exe
    symbol_map.json
    build_info.txt
  v1.0.1/
    app.exe
    symbol_map.json
    build_info.txt
This ensures you can debug any version in production.
Build a tool or script to automatically deobfuscate incoming crash reports:
void main(List<String> args) {
  final trace = File(args[0]).readAsStringSync();
  final symbolMap = json.decode(File('symbol_map.json').readAsStringSync());
  
  final deobfuscated = deobfuscate(trace, symbolMap);
  print(deobfuscated);
}
Before deploying, intentionally trigger errors in an obfuscated build and verify you can successfully deobfuscate the traces.

Symbol Table Statistics

The symbol table tracks the total number of renames:
int get size => _map.length;
You can log this during the build to understand obfuscation coverage:
final symbolTable = result.symbolTable;
print('Obfuscated ${symbolTable.size} identifiers');
Source: lib/src/engine/symbol_table.dart:42
A higher symbol count generally indicates more thorough obfuscation, but also means more entries to manage in your symbol map.

Example: Deobfuscation Script

Here’s a complete script for deobfuscating stack traces:
import 'dart:convert';
import 'dart:io';

void main(List<String> args) {
  if (args.length != 2) {
    print('Usage: dart deobfuscate.dart <trace_file> <symbol_map>');
    exit(1);
  }

  final traceFile = File(args[0]);
  final symbolMapFile = File(args[1]);

  if (!traceFile.existsSync() || !symbolMapFile.existsSync()) {
    print('Error: File not found');
    exit(1);
  }

  final trace = traceFile.readAsStringSync();
  final symbolMap = json.decode(symbolMapFile.readAsStringSync()) 
      as Map<String, dynamic>;

  var deobfuscated = trace;
  symbolMap.forEach((obfuscated, original) {
    deobfuscated = deobfuscated.replaceAll(obfuscated, original as String);
  });

  print(deobfuscated);
}
Usage:
dart run deobfuscate.dart crash.txt symbol_map.json

Next Steps

Troubleshooting

Common issues and solutions

Configuration

Fine-tune obfuscation passes

Build docs developers (and LLMs) love