Skip to content

Diagnostic events

The session's EventBus emits six kinds of structured events. Subscribers — programmatic, via rosetta.events.on(...) — get every event. Trace mode (trace: true) also writes each event to console.error as a single-line formatted string. The two channels coexist; the same event reaches both.

type DiagnosticEvent =
    | ResolveEvent
    | HealthCheckEvent
    | DetectEvent
    | MapLoadEvent
    | SignerCheckEvent
    | MapStatusEvent;

ResolveEvent

Emitted every time the Resolver translates a real name to obfuscated (or misses).

interface ResolveEvent {
    type: 'resolve';
    name: string;
    obfName?: string;
    source: 'cache' | 'map' | 'override';
    miss?: boolean;
    classScope?: string;
    overloadSignature?: string;
}
Field Description
name The real name being resolved (class, method, or field).
obfName The obfuscated name. Present on hits, absent on misses.
source Where this resolution came from. cache = memoized; map = looked up in the map; override = runtime override via rosetta.map.override(...).
miss true if the lookup failed. The Resolver still emits the event before throwing ResolveError / returning a sentinel.
classScope For method/field events, the class real name. Undefined for class-level events.
overloadSignature For method events, the picked overload's JVM signature (e.g. (Landroid/os/Bundle;Lbbbb;)V).

Trace-line formats:

  • Hit (method): [rosetta] com.example.app.IRemoteService$Stub.requestTicket ← c (map) (Landroid/os/Bundle;Lbbbb;)V
  • Hit (class): [rosetta] com.example.app.IRemoteService$Stub ← aaaa (map)
  • Miss: [rosetta] com.example.app.IUnknown ← MISS

Subscribing:

rosetta.events.onType('resolve', (e) => {
    if (e.miss) {
        send({ alert: 'unresolved', name: e.name, scope: e.classScope });
    }
});

Use cases:

  • Failing CI on any miss.
  • Building a map-coverage report after a hook session.
  • Logging a method-call trace for forensic analysis.
  • Detecting cache-vs-map balance for performance debugging.

HealthCheckEvent

Emitted once at session creation, after the attach-time health check completes (unless skipHealthCheck: true).

interface HealthCheckEvent {
    type: 'health-check';
    passed: boolean;
    rate: number;
    failedEntries: readonly string[];
    threshold: number;
}
Field Description
passed rate >= threshold.
rate Fraction of mapped classes that resolved successfully (0.01.0).
failedEntries Real names that failed: the target-namespace guard denied the obfuscated name, or Java.use(obf) threw.
threshold The configured threshold (default 0.8).

Trace-line format:

  • Pass: [rosetta] health-check PASS rate=100.0% threshold=80.0% failures=0
  • Fail: [rosetta] health-check FAIL rate=65.0% threshold=80.0% failures=4

Subscribing:

rosetta.events.onType('health-check', (e) => {
    if (!e.passed) {
        send({
            alert: 'health-check-failed',
            rate: e.rate,
            threshold: e.threshold,
            failed: e.failedEntries,
        });
    }
});

Use cases:

  • Auditing which entries broke between releases.
  • Triggering a map regeneration in CI when the health check drops.
  • Reporting attach-time status to a host controller.

DetectEvent

Emitted once at session creation, after the app + version are determined.

interface DetectEvent {
    type: 'detect';
    app: string;
    version: string;
    source: 'auto' | 'override';
}
Field Description
app The detected (or supplied) Android package name.
version The detected (or supplied) version.
source 'auto' if both app and version came from in-process detection; 'override' if either was supplied via SessionOptions.

Trace-line format:

Subscribing:

rosetta.events.onType('detect', (e) => {
    send({ stage: 'detect', app: e.app, version: e.version, source: e.source });
});

Use cases:

  • Logging the detected version for downstream tools.
  • Asserting in CI that auto-detect picked the expected version.
  • Detecting a version mismatch before the map-version check throws — sometimes you want to handle it gracefully (e.g. fall back to a default map).

MapLoadEvent

Emitted once at session creation, after the map is picked (possibly after registry resolution) and before the health check runs.

interface MapLoadEvent {
    type: 'map-load';
    app: string;
    version: string;
    classCount: number;
    schemaVersion: number;
    selectionKind: 'exact' | 'nearest' | 'code-range' | 'label-range';
}
Field Description
app The map's app field.
version The map's version field. For registry bundles, this is the picked entry's version.
classCount Number of entries in map.classes.
schemaVersion The map's schema_version. Currently always 2.
selectionKind Which tier selected this map: 'exact' (a version_code / label match, also a single-map input), 'nearest' (closest-label fuzzy fallback), 'code-range', or 'label-range' (the opt-in range fallbacks). The four kinds stay distinct rather than collapsing to one boolean, so a deliberate — possibly far — range pick is distinguishable from a nearest guess.

Trace-line format:

Subscribing:

rosetta.events.onType('map-load', (e) => {
    send({ stage: 'map-load', version: e.version, classes: e.classCount });
});

Use cases:

  • Reporting which map version was actually picked (especially after a versionMatch: 'fuzzy' fall-back).
  • Asserting in CI that the expected map made it into the bundle.

SignerCheckEvent

Emitted once at session creation only when the loaded map carries a signer_sha256 and enforcement is on (enforceSigner !== false), after the map is picked and before the health check. When the map has no signer_sha256 — or enforceSigner: false — no event is emitted.

interface SignerCheckEvent {
    type: 'signer-check';
    passed: boolean;
    app: string;
    expected: string;
    actual: readonly string[];
    source: 'signingInfo' | 'signatures';
}
Field Description
passed true if any live signer's SHA-256 matched the map's expected hash. When false, the session throws SignerMismatchError right after emitting this event.
app The session's detected/supplied app package name.
expected The map's expected signer_sha256, normalized (lowercase, no colons). When the map pins an array of hashes (match-any), this is the comma-joined, sorted set.
actual Every live signing-certificate SHA-256 observed (normalized). More than one entry when the app has multiple signers.
source Which PackageManager flag yielded the signers — 'signingInfo' (API 28+ GET_SIGNING_CERTIFICATES) or 'signatures' (pre-28 GET_SIGNATURES).

Trace-line format:

  • Pass: [rosetta] signer-check PASS com.example.app expected=ab… signers=1 (signingInfo)
  • Fail: [rosetta] signer-check FAIL com.example.app expected=cd… signers=2 (signatures)

Subscribing:

rosetta.events.onType('signer-check', (e) => {
    if (!e.passed) {
        send({ alert: 'signer-mismatch', expected: e.expected, actual: e.actual });
    }
});

Use cases:

  • Asserting in CI that the bundled map's signer_sha256 matches the build under test.
  • Recording which signing path (modern vs. legacy) the device used.
  • Catching a repackaged/spoofed build before any hook installs.

See API · Session · Signer enforcement.

MapStatusEvent

Emitted once at session creation only when the loaded map carries a non-active lifecycle status (schema 3, #40), right after the map is picked. A map with status: 'active' (or no status) emits nothing.

interface MapStatusEvent {
    type: 'map-status';
    status: 'superseded' | 'retracted';
    app: string;
    version: string;
    supersededBy?: number;
}
Field Description
status 'superseded' (the map still loads — this is a WARNING) or 'retracted' (the session throws MapRetractedError right after emitting this event).
app The loaded map's app package name.
version The loaded map's version label.
supersededBy The version_code of the replacement map, when the map named one (superseded_by).

Trace-line format:

Subscribing:

rosetta.events.onType('map-status', (e) => {
    if (e.status === 'superseded') {
        console.warn(`map for ${e.app}@${e.version} is superseded`, e.supersededBy);
    }
});

Event ordering

A clean session-creation flow emits events in this order:

sequenceDiagram
    participant S as RosettaSession
    participant L as listener

    S->>L: DetectEvent { source: 'auto' | 'override' }
    S->>L: MapLoadEvent { app, version, classCount }
    opt map.status != 'active'
        S->>L: MapStatusEvent { status, supersededBy }
        Note over S: retracted → throw MapRetractedError
    end
    opt map.signer_sha256 set AND enforceSigner!=false
        S->>L: SignerCheckEvent { passed, expected, actual }
    end
    S->>L: HealthCheckEvent { passed, rate, ... }
    Note over S: ... user hooks install ...
    S->>L: ResolveEvent { name, obfName, source: 'map' }
    S->>L: ResolveEvent { name, obfName, source: 'cache' }
    Note over S: ... hook fires ...
    S->>L: ResolveEvent { name, source: 'cache' }

DetectEvent always precedes MapLoadEvent (you need to know the version to pick a map). MapLoadEvent always precedes the optional MapStatusEvent, then the optional SignerCheckEvent, then HealthCheckEvent (you need a map to check). A retracted MapStatusEvent is the last event the session emits before throwing MapRetractedError. ResolveEvents flow indefinitely after.

Subscribing to all events

const off = rosetta.events.on((event) => {
    send({ rosettaEvent: event });
});

// ... later, unsubscribe ...
off();

rosetta.events.on(fn) returns an unsubscribe function.

EventBus directly

For multi-session scripts or test fixtures, build your own bus:

import { EventBus, createSilentBus, formatEvent } from 'rosetta-frida';

const bus = new EventBus();
bus.setTrace(true);   // print every event to stderr
bus.on((e) => { /* ... */ });
bus.emit({ type: 'detect', app: 'com.example.app', version: '1.0.0', source: 'auto' });

createSilentBus() is a one-line helper that returns an EventBus with trace explicitly off — handy for tests.

formatEvent(event) exposes the canonical single-line formatter used by trace mode, so you can re-use it elsewhere.