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.0–1.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:
[rosetta] detect auto: [email protected][rosetta] detect override: [email protected]
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:
[rosetta] map-load [email protected] schema=4 classes=15 select=exact
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_sha256matches 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:
- Superseded:
[rosetta] map-status SUPERSEDED [email protected] superseded_by=99 - Retracted:
[rosetta] map-status RETRACTED [email protected]
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.