Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,50 @@ return an explicit error. Codex owns layout and button labels, so the native for
does not reproduce custom footer buttons from a design mockup. Thumbnails are
illustrations, not captured app screens.

## Embedded TraceSift explorer

Pressing **Explore** on a card opens TraceSift's full explorer in the card's own
frame rather than a browser window. The renderer is not a copy: both TraceSift's
web page and this plugin import the same components from
`@callstack/tracesift-explore`, so a drill-down cannot drift from the app that
produced the numbers.

The card's frame cannot reach the TraceSift service — Codex strips plain HTTP
origins from widget CSP — so its data rides MCP like everything else here.
`mobile_tracesift_explore_payload` fetches `/api/integration/v1/task` or
`/api/integration/v1/react-explore` with the service bearer token and returns the
payload as one text item plus a small `structuredContent` envelope
(`apiVersion`, `kind`, `encoding`, `payloadBytes`). A full task card carries its
whole call tree and timeline, which runs to megabytes, so past 64 KiB the text is
gzip-compressed base64; a measured 25 MB tree crosses in about 8 ms at a fifth
of its JSON size. Below that threshold it stays plain JSON. Keeping the tree out
of structured content avoids the deep-structured-payload failure documented
above: the SDK and host validate only the envelope, and the iframe inflates and
parses the payload once into the shapes the explorer draws. Payloads above
24 MiB are refused before decoding and point at the browser window; the service
already caps its trees at 60k nodes and 5k commits. Payload size is reported as
`tracesift.explore.payload_bytes`, which is where an oversized tree shows up
before a reader reports a spinner.

The same rule applies to the payload's age: it is fetched from the live service,
so an expired analysis answers with the existing "run it again" error instead of
a half-drawn tree. **Open in browser** keeps the previous path, which hands the
selection to `tracesift explore`.

Version skew favours the reader's installed service: payloads are additive-only
within integration API v1, the explorers ignore fields they do not know, and an
older service that cannot answer the endpoint fails with an update message. The
plugin folds the package's one compiled stylesheet into its single HTML bundle,
after the plugin's own Tailwind layer, so neither build compiles the other's
sources. That sheet opens by writing its theme tokens to `:root` and `.dark`,
which would recolor the plugin's cards behind the explorer, so the fold scopes
both token blocks to `.tracesift-explore` — descendants inherit the custom
properties, and a block that no longer has the shape the fold expects fails the
build rather than leaking globally. During development the package is wired in
with a local `file:` link; the build then resolves its imports from this
project's install whenever the versions agree, so the bundle never carries a
second React or a duplicated copy of a shared library.

## Sources

The implementation follows [Baguette's HTTP and WebSocket routes](https://github.com/tddworks/baguette/blob/v0.2.1/docs/serve.md) and [gesture protocol](https://github.com/tddworks/baguette/blob/v0.2.1/docs/wire.md). The bundled automation server follows [agent-device's official MCP setup](https://github.com/callstack/agent-device#add-mcp-tools-to-your-agent). Native panel metadata follows [OpenAI's plugin extensions](https://developers.openai.com/plugins/build/extensions). The package uses the [portable plugin format](https://developers.openai.com/plugins/build/plugins).
2 changes: 1 addition & 1 deletion docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Since 0.1.99, `ui.logs.query_parse` measures query compilation in milliseconds o

The React UI reports to `codex-mobile-dev-ui` (project `4512181027471440`). The main Node MCP server and the agent-device launcher report to `codex-mobile-dev-server`, distinguished by the `component` attribute. Native helpers report to `codex-mobile-dev-native`: Baguette, physical iOS mirroring, iOS FPS and logs, and Android CPU and FPS collectors. All three projects use release `mobile-dev@<plugin version>`.

TraceSift cards use the `tracesift` surface. `tracesift.operation.duration` measures local service readiness as part of analysis, profile upload and analysis, stable host-view registration when the host supplies it, persisted-analysis restoration from either a host view or replayed tool input, handoff generation, and invoking `tracesift explore` with a persisted compact analysis, in milliseconds; `tracesift.operation.samples` records the bounded operation and success/failure outcome. `ui.tracesift.process` measures result validation, `ui.tracesift.remember` measures the app-to-server registration round trip for a live card, `ui.tracesift.restore` measures the app-to-server restore round trip after a card view is recreated, `ui.tracesift.handoff` measures the app-to-server handoff round trip, and `ui.tracesift.message_ack` ends when the host acknowledges the investigation message. `ui.tracesift.explore` measures the app-to-server round trip through successful CLI browser launch for either a CPU task or a React commit; full Explore data remains in the local TraceSift service. A card-count gauge and bounded counters cover registration successes and failures, restore hits, misses and failures, Investigate actions, Explore openings and failures, hand-off copies and copy failures, handoff failures, composer unavailability, send failures, and unconfirmed delivery. Readiness, shared interaction, visibility, and cleanup measurements use the existing UI aggregation. Profile paths and contents, filenames, card text, frame and file names, task, root, and commit indices, analysis/card/host-view identifiers, handoffs, service URLs, process identifiers, tokens, and TraceSift diagnostics are never added to telemetry; replayed profile inputs are hashed only for local lookup. Expected setup, validation, expiry, cancellation, and version outcomes use existing expected-error classification; unexpected transport and malformed-response failures retain handled server/UI error coverage. These measurements describe plugin and local-service work, not the profiled app's performance.
TraceSift cards use the `tracesift` surface. `tracesift.operation.duration` measures local service readiness as part of analysis, profile upload and analysis, stable host-view registration when the host supplies it, persisted-analysis restoration from either a host view or replayed tool input, handoff generation, and invoking `tracesift explore` with a persisted compact analysis, in milliseconds; `tracesift.operation.samples` records the bounded operation and success/failure outcome. `ui.tracesift.process` measures result validation, `ui.tracesift.remember` measures the app-to-server registration round trip for a live card, `ui.tracesift.restore` measures the app-to-server restore round trip after a card view is recreated, `ui.tracesift.handoff` measures the app-to-server handoff round trip, and `ui.tracesift.message_ack` ends when the host acknowledges the investigation message. `ui.tracesift.explore` counts opening the embedded explorer, and `ui.tracesift.payload` measures that app-to-server round trip, which carries the finding's full call tree or commit timeline through MCP; `ui.tracesift.payload_bytes` records how large those delivered payloads are, and `tracesift.operation` adds a `payload` operation for the same fetch on the server. Since Explore renders in-panel, the older meaning of `ui.tracesift.explore` (a successful CLI browser launch) now belongs to the `ui.tracesift.explore_browser` counter and its `ui.tracesift.explore_browser` timing and `ui.tracesift.explore_browser_failed` counter. Explorer readiness reuses `ui.surface.ready`, and `tracesift_view` distinguishes `cards` from `explorer` so explorer work is not attributed to the card list; navigating back resets that context. A card-count gauge and bounded counters cover registration successes and failures, restore hits, misses and failures, Investigate actions, Explore openings and failures, hand-off copies and copy failures, handoff failures, composer unavailability, send failures, and unconfirmed delivery. Readiness, shared interaction, visibility, and cleanup measurements use the existing UI aggregation. Profile paths and contents, filenames, card text, frame and file names, task, root, and commit indices, analysis/card/host-view identifiers, handoffs, service URLs, process identifiers, tokens, and TraceSift diagnostics are never added to telemetry; replayed profile inputs are hashed only for local lookup. Expected setup, validation, expiry, cancellation, and version outcomes use existing expected-error classification; unexpected transport and malformed-response failures retain handled server/UI error coverage. These measurements describe plugin and local-service work, not the profiled app's performance.

The environments are `development` and `release`. `npm run build` and `npm run package` default to `development`, including local installed packages. For a public release, run `npm run build:release` followed by `npm run package:release`. Packaging rejects a build from the other environment. The package stores its environment in `dist/telemetry-environment.json`; Node telemetry, native helpers and the served UI use that setting. Live reload does not determine the environment. Set `MOBILE_DEV_ENVIRONMENT=development` or `MOBILE_DEV_ENVIRONMENT=release` in the MCP launch environment to override explicitly, then restart the MCP processes and reopen the panel.

Expand Down
111 changes: 110 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@
},
"dependencies": {
"@base-ui/react": "^1.8.0",
"@callstack/tracesift-explore": "file:../../../../../../Desktop/callstack/open-source/perf-ai/packages/explore",
"@fontsource-variable/inter": "^5.3.0",
"@legendapp/list": "^3.6.0",
"@modelcontextprotocol/ext-apps": "1.7.5",
"@modelcontextprotocol/sdk": "1.31.0",
"@openai/mcp-extensions": "0.1.0",
"@rozenite/ui": "2.4.0",
"@sentry/node": "11.2.0",
"@sentry/react": "11.2.0",
"ajv": "8.20.0",
Expand Down
15 changes: 13 additions & 2 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { androidCpuSourceHash } from "./build-android-cpu.mjs";
import { baguetteTelemetrySourceHash } from "./rebuild-baguette.mjs";
import { telemetryBuildEnvironment } from "./telemetry-build.mjs";
import { buildServeEmu } from "./build-serve-emu.mjs";
import { exploreUILayerCSS } from "./explore-css.mjs";
import { sharedNodeModules } from "./shared-node-modules.mjs";
import { build } from "esbuild";
import { sentryEsbuildPlugin } from "@sentry/node/esbuild";
import SentryCli from "@sentry/cli";
Expand Down Expand Up @@ -110,8 +112,12 @@ await writeFile("skills/agent-device/references/workflow.md", workflow);
await buildServeEmu("dist/serve-emu");
const app = await build({
entryPoints: ["src/ui/app.tsx"], jsx: "automatic", define: { "process.env.NODE_ENV": '"production"' },
// The Explore package arrives as a `file:` link during development, and esbuild
// resolves a link to its real path, where the TraceSift checkout has its own
// React. One page may own exactly one React, so both are pinned to this one.
alias: { react: resolve("node_modules/react"), "react-dom": resolve("node_modules/react-dom") },
loader: { ".woff2": "dataurl", ".woff": "dataurl" },
plugins: [{ name: "shadcn-theme", setup(build) {
plugins: [sharedNodeModules("."), { name: "shadcn-theme", setup(build) {
build.onLoad({ filter: /theme\.css$/ }, async ({ path }) => {
const compiler = await compile(await readFile(path, "utf8"), { base: dirname(path), onDependency() {} });
const scanner = new Scanner({ sources: [{ base: resolve("src/ui"), pattern: "**/*.{ts,tsx}", negated: false }] });
Expand All @@ -127,9 +133,14 @@ for (const file of app.outputFiles) await writeFile(file.path, file.contents);
const cli = new SentryCli();
await cli.execute(["sourcemaps", "inject", ".sentry/ui"]);
const js = await readFile(".sentry/ui/app.js", "utf8");
const css = app.outputFiles.find(file => file.path.endsWith(".css"))?.text ?? "";
// The Explore view's own sheet is already compiled by its package, so it joins
// the inlined styles after the plugin's foundation rather than through it.
const css = `${app.outputFiles.find(file => file.path.endsWith(".css"))?.text ?? ""}\n${await exploreUILayerCSS()}`;
const template = await readFile("src/ui/index.html", "utf8");
const configuredTemplate = template.replace('name="mobile-dev-environment" content="development"', `name="mobile-dev-environment" content="${telemetryEnvironment}"`);
// The inlined sheets must not be able to close their own tag: esbuild escapes
// the script side, and the explore package's compiled CSS needs the same check.
if (css.includes("</style")) throw new Error("An inlined stylesheet contains a </style sequence and cannot be folded into app.html.");
await writeFile("dist/app.html", configuredTemplate
.replace("<!-- APP_STYLE -->", () => `<style>${css}</style>`)
.replace("<!-- APP_SCRIPT -->", () => `<script>${js.replace(/<\/script/gi, "<\\/script")}\n//# sourceURL=app:///mobile-dev-ui.js\n</script>`));
Expand Down
Loading
Loading