x-pack/solutions/observability/plugins/client_apps/README.md
Plugin that complements client applications' dashboards. It currently allows retracing obfuscated errors back to human-readable source code references. It is designed to accommodate multiple platforms (Android, JavaScript, iOS) within the same plugin, with each platform defining its own backend services and UI independently.
The plugin is organized into three top-level areas:
common/ — Types and API path constants shared between client and server.public/ — Client-side code: the Kibana app registration, a router, and one view
component per platform action.server/ — Server-side code: route registration, the libraries in
lib/, and shared utilities.Each platform owns its directory under server/platforms/ and
public/platforms/. Platform code does not import from other platforms.
This feature allows users to understand their app's errors by converting obfuscated stacktraces into human-readable ones using a pre-uploaded mapping file. Client apps tend to emit obfuscated errors for optimization and security reasons, making them difficult to debug.
Two things must be in place before the plugin can retrace a stacktrace:
Mapping documents uploaded. The platform's retrace maps must be uploaded to an Elasticsearch index accessible by Kibana before retracing can succeed. When and how this upload happens is platform-specific. Refer to each platform's upload tooling for details.
Build identifier present in the log event. Each crash or error document must carry a platform-specific build identifier field that the plugin uses to locate the correct mapping index (preferably using OpenTelemetry's app.build_id attribute). The platform's agent or SDK is responsible for populating this field at runtime. Without it, the plugin cannot determine which mapping to use and will return an error.
Each platform needs four structural pieces:
server/platforms/<platform>/routes.ts and register it in
server/plugin.ts.common/index.ts.public/platforms/<platform>/.<Route> for the new path in public/app.tsx.The new platform will be reachable at /app/clientApps/<platform>/<action> via URL
drilldown from any Kibana dashboard.
The retrace algorithm for each platform lives in server/lib/. The pattern is:
Retracer<MapType> class from server/lib/retracer.ts, providing
the platform-specific document type as the generic parameter.retrace() to parse the stacktrace, call this._fetcher.fetch(identifiers)
to retrieve mapping documents in a single batch, and return the retraced stacktrace.RetraceMapFetcher<MapType> interface is the only connection between the algorithm
and storage. The route handler constructs a concrete fetcher that queries Elasticsearch
using esClient.mget or similar; tests pass an in-memory fetcher instead.The route handler then wires everything together: it receives { stacktrace, build_id },
constructs the ES fetcher (scoped to the correct index for that build), creates the retracer,
calls retrace(), and returns RetraceResponse ({ original, retraced }).
Each server/platforms/<platform>/ directory can contain as many modules as needed. Add
new modules there and wire them into the platform's route handler, or add a new route file
if the service needs its own endpoint. Add any new API path constants to common/index.ts.
Utilities needed by more than one platform belong in:
server/lib/ for server-side helpers (error handling, retrace libraries, ES client
wrappers)common/ for types and constants shared between server and clientThe plugin registers with visibleIn: [], meaning that it does not appear in Kibana's navigation menu.
Users reach platform views via URL drilldowns configured on dashboard panels. Each platform
action defines the query parameters it needs to locate the source event.
{{kibanaUrl}}/app/clientApps/<platform>/<action>?<platform-specific-query-params>
For Android crash retracing, the drilldown must provide the crash event identity. Kibana's URL
drilldown templates can't reference arbitrary field names directly, so the source panel must be
a table with session.id, @timestamp, and app.build_id as columns (in that order), and the
drilldown must use the Row click trigger, which exposes every column of the clicked row as
event.values, indexed by column position. The route uses these values to find the crash
document and read its stacktrace and app.build_id:
{{kibanaUrl}}/app/clientApps/android/retrace?session_id={{event.values.[0]}}×tamp={{event.values.[1]}}&app_build_id={{event.values.[2]}}
For environments where crash documents are written outside the default
logs-generic.otel* data stream, the drilldown can include a free-form Elasticsearch
index pattern:
{{kibanaUrl}}/app/clientApps/android/retrace?session_id={{event.values.[0]}}×tamp={{event.values.[1]}}&app_build_id={{event.values.[2]}}&index=logs-myapp.otel*
The @timestamp column must not have a custom display format applied, since the route matches
it with an exact Elasticsearch term query that requires the value to parse to the same instant
as the indexed one. The index value is passed to Elasticsearch as the current Kibana user. It
is intentionally free-form so deployments can route crash events to custom data streams or
aliases, but the user must still have Elasticsearch index privileges for the requested pattern.
Android R8 mapping data is
stored in per-build Elasticsearch indices named
.android-r8-mappings-<build_id>, where build_id is read from the crash document. Each
index contains one document per obfuscated class, with _id = SHA-256(obfuscated_class).
Within each class document, methods is an opaque payload keyed by obfuscated method name.
Each method contains ranged mappings and, when R8 emitted entries without obfuscated line
ranges, optional default_mappings. default_mappings are used only when the method has no
ranged mappings at all; an out-of-range line on a ranged method is left unchanged.
R8 extras
are forwarded as native JSON under mappings[].extras. The retracer handles known
extras such as outline, outlineCallsite, rewriteFrame, and synthesized, and ignores
unknown extra IDs or fields so newer R8 metadata can flow through without changing the index
mapping.
# Unit tests
node scripts/jest x-pack/solutions/observability/plugins/client_apps
# Type checking
node scripts/type_check --project x-pack/solutions/observability/plugins/client_apps/tsconfig.json