Files
software-mansion__argent/packages/tool-server/test/telemetry-device-kind.test.ts
filip131311 c573e9c524 feat(telemetry): report device_kind next to platform on tool events (#1049)
## Why

Telemetry cannot tell a physical iPhone from a simulator: `platform` is
`ios` for both, and the tool-server's `kind` never reaches the events.
Physical-iOS support shipped in 0.24.0 with no way to count its adoption
except through `ios_device_*` failure stages on `tool:fail`. Physical
Android phones have the same gap.

## What

`tool:invoke` / `tool:complete` / `tool:fail` gain `device_kind:
simulator | emulator | vvd | device | app`, derived from the same device
id as `platform` (single classifier in `telemetry-platform.ts`, which
`debugger:tool_outcome` and `lens:*` also go through for their
platform). `platform: ios` + `device_kind: device` is a physical iPhone.

The kind is emitted only for positively recognised id shapes, so the
metric is not polluted by ids that are not devices:

| id shape | platform | device_kind |
|---|---|---|
| simulator UUID / `remote:` | ios / ios-remote (tvos when the cache is
warm) | simulator |
| physical iPhone UDID | ios | device |
| `emulator-NNNN` | android (android-tv when warm) | emulator |
| USB serial, 6–20 alnum with a digit and a letter | android | device |
| non-loopback `ip:port` | android | device (best effort) |
| loopback `127.0.0.1:port` / `localhost:port` / `::1:port` | android |
emulator |
| anything else non-iOS-shaped (Metro 40-hex handle, `booted`,
`Pixel_7`, IPv6, mDNS) | android | omitted |
| `chromium-cdp-<port>` / `amazon-…` | chromium / vega | app / vvd |
| `avdName`-only boot | android | omitted |

A child sub-tool that names its own device replaces the parent's
platform, kind and provider label wholesale, so an `avdName`-only child
never reports the parent's `simulator` next to its own `android`. A
logical-keyed Metro session strips the kind.

`unknown` (in the registry `DeviceKind`) is rejected by the sanitizer
like `platform: "unknown"`; no registry change.

## Querying

- Physical iPhone adoption: `tool:invoke` where `platform = 'ios' and
device_kind = 'device'`, distinct users over it. `isIosPhysicalUdid` is
a positive match, so invoke is clean.
- Physical Android: count on `tool:complete` (the device answered).
`tool:invoke` / `tool:fail` still carry residual mistakes that happen to
look like a serial (`Pixel7`), and a non-loopback `ip:port` may be a
networked virtual device.

## Docs

- `Telemetry.md` → 1.04, effective date 7 September 2026 (adjust to the
release date on merge). Discloses the new category and, while touching
that section, the external-provider label that #735 added without a
notice line — happy to drop that second bullet if it should ship
separately.
- No docs-site change: `docs/reference/telemetry.mdx` links to
`Telemetry.md` and has no per-property list.

## Notes

- Pre-existing, unchanged: HTTP-edge failures carry no
`tool_invocation_id`; `stop-all-simulator-servers` declares no
capability, so its invoke row has neither field while its fail row has
both. `device_kind` follows `platform` in each site.
- Legacy 40-hex iPhone UDIDs already classify as `android` today; they
now get no kind rather than a wrong one.
- Design went through three adversarial review rounds before
implementation and one on the diff; the fallback-bucket and
child-attribution rules above came out of those.

## Verification

- `packages/telemetry`: 319 tests, `packages/tool-server`: full suite
5670 passed / 1 skipped; `typecheck:tests` both packages; `eslint
--max-warnings 0`; `knip`; prettier.
- New: `test/telemetry-device-kind.test.ts` (the shape table above),
plus cases in `http-tools-meta`, `http-platform-alias`, `sanitize`,
`registry-listener`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01BSGpeAwvoCv2JAhbLsuto2


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Telemetry now records a device category—such as simulator, emulator,
physical device, virtual device, or desktop app—alongside the platform.
* Device categories are inferred from device information and may include
a short label for external device providers.
* Device category information is included in tool invocation,
completion, and failure telemetry when available.
* Device identifiers themselves continue to be excluded from telemetry.
* **Documentation**
* Updated the Argent Privacy Notice (Telemetry) to version 1.04,
effective 7 September 2026.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-07 11:55:57 +02:00

69 lines
3.3 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
attributeDeviceForTelemetry,
classifyDeviceForTelemetry,
} from "../src/utils/telemetry-platform";
// Cache-only readers: cold here, so every platform stays coarse and the kind can
// be pinned independently of the TV refinement.
vi.mock("../src/utils/ios-devices", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/utils/ios-devices")>();
return { ...actual, getCachedSimulatorRuntimeKind: () => undefined };
});
vi.mock("../src/utils/adb", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/utils/adb")>();
return { ...actual, getCachedAndroidRuntimeKind: () => undefined };
});
const SIM_UDID = "11111111-1111-1111-1111-111111111111";
const PHYSICAL_UDID = "00008030-000A1B2C3D4E5F60";
const METRO_HANDLE = "8b9223b1392be193fa9058e0cef5cefb2bddeb68";
describe("attributeDeviceForTelemetry", () => {
it.each([
// Positive shapes `resolveDevice` already recognises.
[SIM_UDID, { platform: "ios", device_kind: "simulator" }],
[PHYSICAL_UDID, { platform: "ios", device_kind: "device" }],
[`remote:${SIM_UDID}`, { platform: "ios-remote", device_kind: "simulator" }],
["emulator-5554", { platform: "android", device_kind: "emulator" }],
["chromium-cdp-9222", { platform: "chromium", device_kind: "app" }],
["amazon-1a2b3c", { platform: "vega", device_kind: "vvd" }],
// Android hardware: USB serials with a digit and a letter, non-loopback ip:port.
["R5CT12345678", { platform: "android", device_kind: "device" }],
["HT82A0203045", { platform: "android", device_kind: "device" }],
["192.168.1.5:5555", { platform: "android", device_kind: "device" }],
// Loopback ip:port is an emulator console slot (adb.ts consolePortFromAdbSerial).
["127.0.0.1:5555", { platform: "android", device_kind: "emulator" }],
["localhost:5555", { platform: "android", device_kind: "emulator" }],
["::1:5555", { platform: "android", device_kind: "emulator" }],
// `ext:` ids attribute by their native id.
[`ext:acme-3f2a9c:${PHYSICAL_UDID}`, { platform: "ios", device_kind: "device" }],
["ext:acme-3f2a9c:emulator-5554", { platform: "android", device_kind: "emulator" }],
["ext:acme-3f2a9c:R5CT12345678", { platform: "android", device_kind: "device" }],
])("%s → %o", (id, expected) => {
expect(attributeDeviceForTelemetry(id)).toEqual(expected);
});
it.each([
// `resolveDevice` sends every one of these to android/device; none is hardware.
METRO_HANDLE, // 40-hex Metro logicalDeviceId, also a legacy iPhone UDID shape
"oops",
"booted",
"Pixel_7",
"abcdefgh",
"1234567890",
"[fe80::1]:5555",
"pixel.local:5555",
"adb-R5CT12345678-AbCdEf._adb-tls-connect._tcp.",
"0123456789ABCDEF0123456789ABCDEF", // 32-hex, over the 20-char cap
])("%s carries the fallback platform but no kind", (id) => {
expect(attributeDeviceForTelemetry(id)).toEqual({ platform: "android" });
});
it("classifyDeviceForTelemetry is the platform half of the same classifier", () => {
for (const id of [SIM_UDID, PHYSICAL_UDID, "emulator-5554", "R5CT12345678", METRO_HANDLE]) {
expect(classifyDeviceForTelemetry(id)).toBe(attributeDeviceForTelemetry(id).platform);
}
});
});