@xenon-device-management/xenon 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -26
- package/lib/package.json +2 -1
- package/lib/public/assets/{AnnotationOverlay-Btq3dZRI.js → AnnotationOverlay-2W4JvEyZ.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-C9OoAgg_.js → ApiKeyGate-GmckX_7r.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-DBC_xczB.js → DeviceMosaicView-DqODzkyo.js} +1 -1
- package/lib/public/assets/{EmptyState-DqWU2-vM.js → EmptyState-7hETKEWb.js} +1 -1
- package/lib/public/assets/{FieldGroup-CveQ3xrA.js → FieldGroup-GOgssFEP.js} +1 -1
- package/lib/public/assets/{FilterMenu-BfKDSfHz.js → FilterMenu-Yvf6RGMd.js} +1 -1
- package/lib/public/assets/{Menu-CtdCybzA.js → Menu-KrPnfdsf.js} +1 -1
- package/lib/public/assets/{Modal-Bsvj1tm2.js → Modal-C6WjZFeI.js} +1 -1
- package/lib/public/assets/{RecordingPage-CRofOiyx.js → RecordingPage-CSRIhZEh.js} +1 -1
- package/lib/public/assets/{RecordingsPage-jNQHb2IR.js → RecordingsPage-CB8ST3nE.js} +1 -1
- package/lib/public/assets/{SegmentedControl-C8HtTha-.js → SegmentedControl-C9o90KId.js} +1 -1
- package/lib/public/assets/{SettingCard-D8K1LZNo.js → SettingCard-CPm3w9l2.js} +1 -1
- package/lib/public/assets/{Table-zNTX92Pn.js → Table-DwIgERRt.js} +1 -1
- package/lib/public/assets/{activity-CJ8LwZq9.js → activity-DrLbHifn.js} +1 -1
- package/lib/public/assets/{ai-settings-CgxQZqrc.js → ai-settings-DU_Ls06P.js} +1 -1
- package/lib/public/assets/{api-keys-ECpo-zTo.js → api-keys-CWYdEzJ_.js} +1 -1
- package/lib/public/assets/{apps-BjGymyf3.js → apps-BqNVdln7.js} +1 -1
- package/lib/public/assets/{arrow-left-BWfCbeHP.js → arrow-left-BtB0_Jg_.js} +1 -1
- package/lib/public/assets/{arrow-right-DC7UGf_n.js → arrow-right-C8NhdtyN.js} +1 -1
- package/lib/public/assets/{arrow-up-right-BOjwybDk.js → arrow-up-right-gMg0ffvv.js} +1 -1
- package/lib/public/assets/{auth-shell-CxMTo9aH.js → auth-shell-CiYZ3N4M.js} +2 -2
- package/lib/public/assets/{builds-page-DOCm4HkA.js → builds-page-EaaqDmLN.js} +1 -1
- package/lib/public/assets/{button-Cg08-EMT.js → button-DDau4Axv.js} +1 -1
- package/lib/public/assets/{calendar-CA1ikLJs.js → calendar-DQ83B1ed.js} +1 -1
- package/lib/public/assets/{check-BMTaH72W.js → check-K1ovTQam.js} +1 -1
- package/lib/public/assets/{chevron-right-B2QOv9Nx.js → chevron-right-Biq00pMt.js} +1 -1
- package/lib/public/assets/{circle-check-CYXBoUqT.js → circle-check-CBv88cH7.js} +1 -1
- package/lib/public/assets/{circle-x-DTwnSAnl.js → circle-x-Bmltailn.js} +1 -1
- package/lib/public/assets/{clock-C8_HViQP.js → clock-BPrld-3h.js} +1 -1
- package/lib/public/assets/{copy-Be7LNMXV.js → copy-t0QB14JI.js} +1 -1
- package/lib/public/assets/{device-explorer-BOqtfGXz.js → device-explorer-BgGZ8AvV.js} +1 -1
- package/lib/public/assets/{download-B56lwxWP.js → download-BroHxNOh.js} +1 -1
- package/lib/public/assets/{forgot-password-CCU6bzLn.js → forgot-password-Bjmf49i6.js} +1 -1
- package/lib/public/assets/{h264Stream-C-OtmIuk.js → h264Stream-DeD__6wa.js} +1 -1
- package/lib/public/assets/{index-CfxsUslp.js → index-72oF89LJ.js} +1 -1
- package/lib/public/assets/{index-B9KQ6LsY.js → index-B0JeuZbg.js} +1 -1
- package/lib/public/assets/index-B0gQkEg0.js +219 -0
- package/lib/public/assets/{input-CZHFjbSg.js → input-DIc4MSC7.js} +1 -1
- package/lib/public/assets/{line-chart-Gl_Dz3X_.js → line-chart-BcWm2vW8.js} +1 -1
- package/lib/public/assets/{list-checks-CCgjeyGY.js → list-checks-B06IJJad.js} +1 -1
- package/lib/public/assets/{lock-CkOwgDcx.js → lock-pVLvnN30.js} +1 -1
- package/lib/public/assets/{login-D5cc4w7M.js → login-CN77vy--.js} +1 -1
- package/lib/public/assets/{maintenance-settings-t7qr3fvX.js → maintenance-settings-CSdzQSkY.js} +1 -1
- package/lib/public/assets/{monitor-DEJixKQO.js → monitor-BtrN5IkI.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-CtOrk4M8.js → mouse-pointer-2-CNsQfo6w.js} +1 -1
- package/lib/public/assets/{network-Dii9qdvk.js → network-BSfA8ZxG.js} +1 -1
- package/lib/public/assets/{overview-rolAACrO.js → overview-BfTebCK5.js} +1 -1
- package/lib/public/assets/{page-header-COQZxJfR.js → page-header-D8KUX6Qg.js} +1 -1
- package/lib/public/assets/{play-DrN1Se_R.js → play-Cvc8xzic.js} +1 -1
- package/lib/public/assets/{plus-5aq-9uRx.js → plus-C9Zj84UF.js} +1 -1
- package/lib/public/assets/{profile-page-6i5kY6kF.js → profile-page-Bi27STgs.js} +1 -1
- package/lib/public/assets/{reset-password-sp2IBrc7.js → reset-password-CPsOuwSu.js} +1 -1
- package/lib/public/assets/{runbook-page-DJDjeopU.js → runbook-page-DEQ-DBOX.js} +153 -102
- package/lib/public/assets/{select-Bpyn7rWO.js → select-BWpnhWXj.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BH74vmKv.js → selector-detail-redirect-BefSCSpe.js} +1 -1
- package/lib/public/assets/{selector-health-page-Cw_g218H.js → selector-health-page-MSk96Zz7.js} +1 -1
- package/lib/public/assets/session-detail-page-DPyBNef_.js +4 -0
- package/lib/public/assets/{settings-C3uyGhyc.js → settings-V6x-cXwy.js} +1 -1
- package/lib/public/assets/{stat-tile-xVqAKIlI.js → stat-tile-Duex-GZD.js} +1 -1
- package/lib/public/assets/{tablet-BxUScAeO.js → tablet-BBcoqBmL.js} +1 -1
- package/lib/public/assets/{teams-B5ljGkY2.js → teams-ts8p5rEh.js} +1 -1
- package/lib/public/assets/{trash-2-B701i8sy.js → trash-2-CZ2ew6AZ.js} +1 -1
- package/lib/public/assets/{upload-C2gr44RS.js → upload-Dv99y_le.js} +1 -1
- package/lib/public/assets/{use-builds-data-NeQOFEoy.js → use-builds-data-DrYC8JCF.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DEzJq5ef.js → use-password-reset-mode-C7QkBNkP.js} +1 -1
- package/lib/public/assets/{users-D_vv_yz4.js → users-D7tKMESH.js} +1 -1
- package/lib/public/assets/{users-MY6eNe6s.js → users-gJ5Y01yo.js} +1 -1
- package/lib/public/assets/{video-off-OK1lNZQn.js → video-off-CqZVuZGs.js} +1 -1
- package/lib/public/assets/{webhook-settings-BU-C9lBf.js → webhook-settings-DI5DSW99.js} +1 -1
- package/lib/public/assets/{zap-DO9ry1iz.js → zap-ClcHFxfo.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/schema.json +11 -7
- package/lib/src/InternalHttpClient.js +27 -5
- package/lib/src/XenonCapabilityManager.js +10 -3
- package/lib/src/app/openapi/sessions.yaml +7 -5
- package/lib/src/app/routers/auth.js +2 -1
- package/lib/src/app/routers/users.js +2 -1
- package/lib/src/app/ws/nodeSocketRelay.js +77 -11
- package/lib/src/dashboard/commandLogFields.js +42 -0
- package/lib/src/dashboard/event-manager.js +26 -154
- package/lib/src/dashboard/services/failure-analysis-service.js +8 -86
- package/lib/src/dashboard/services/failureCategories.js +264 -0
- package/lib/src/device-managers/AndroidDeviceManager.js +61 -23
- package/lib/src/device-managers/IOSDeviceManager.js +10 -0
- package/lib/src/device-utils.js +14 -6
- package/lib/src/gateway/forwardToNode.js +4 -11
- package/lib/src/gateway/hubSessionToken.js +27 -4
- package/lib/src/gateway/nodeWebDriverUrl.js +3 -5
- package/lib/src/helpers/outboundProxy.js +219 -0
- package/lib/src/index.js +21 -0
- package/lib/src/interceptors/CommandInterceptor.js +13 -15
- package/lib/src/interceptors/xenonScripts.js +28 -7
- package/lib/src/logger.js +3 -1
- package/lib/src/plugin.js +8 -3
- package/lib/src/scripts/database-check.js +3 -3
- package/lib/src/scripts/initialize-database.js +35 -33
- package/lib/src/scripts/run-migrations.js +1079 -27
- package/lib/src/services/ApiKeyService.js +16 -5
- package/lib/src/services/InterceptorService.js +6 -2
- package/lib/src/services/OrphanSweeper.js +6 -0
- package/lib/src/services/SelectorStateService.js +11 -7
- package/lib/src/services/SelectorVerificationJob.js +6 -2
- package/lib/src/services/SessionLifecycleService.js +32 -18
- package/lib/src/services/SocketClient.js +11 -0
- package/lib/src/services/SocketServer.js +361 -42
- package/lib/src/services/TeamService.js +5 -1
- package/lib/src/services/TracingService.js +171 -60
- package/lib/src/services/UserService.js +14 -3
- package/lib/src/services/UserSessionService.js +16 -4
- package/lib/src/services/healing/HealingOrchestrator.js +3 -1
- package/lib/src/services/identity/identityChanges.js +41 -0
- package/lib/src/services/logcat/SessionDeviceLogs.js +97 -10
- package/lib/src/services/logcat/deviceLogBook.js +5 -4
- package/lib/src/services/logcat/iosDriverLog.js +178 -0
- package/lib/src/services/selector-health/SelectorVisibilityResolver.js +121 -0
- package/lib/src/services/token/JwtKeyService.js +8 -2
- package/lib/src/services/token/mcpTokenTtl.js +27 -0
- package/lib/src/sessions/LocalSession.js +29 -1
- package/lib/src/sessions/RemoteSession.js +12 -2
- package/lib/src/sessions/SessionManager.js +5 -8
- package/lib/test/helpers/driver-log.js +38 -0
- package/lib/test/helpers/fake-http-proxy.js +106 -0
- package/lib/test/helpers/otlp-probe.js +107 -0
- package/lib/test/integration/androidDevices.spec.js +1 -1
- package/lib/test/integration/ios/01iOSSimulator.spec.js +1 -1
- package/lib/test/integration/ios/02iOSDevices.spec.js +1 -1
- package/lib/test/integration/selector-state-flow.spec.js +5 -5
- package/lib/test/unit/XenonCapabilityManager.spec.js +23 -0
- package/lib/test/unit/android-adb-retry.spec.js +112 -0
- package/lib/test/unit/command-log-network-capture.spec.js +191 -0
- package/lib/test/unit/dashboard-events-device-logs.spec.js +59 -11
- package/lib/test/unit/dashboard-events-team-scope.spec.js +6 -3
- package/lib/test/unit/database-check.spec.js +1 -0
- package/lib/test/unit/device-utils.spec.js +13 -4
- package/lib/test/unit/event-log-session-data.spec.js +11 -4
- package/lib/test/unit/failure-analysis.spec.js +22 -8
- package/lib/test/unit/failure-categories.spec.js +24 -22
- package/lib/test/unit/failure-categorization.spec.js +393 -0
- package/lib/test/unit/healing-tier-gate.spec.js +3 -2
- package/lib/test/unit/hub-node-gateway-appium-server.spec.js +72 -1
- package/lib/test/unit/ios-driver-log.spec.js +168 -0
- package/lib/test/unit/ios-simulator-session-readiness.spec.js +187 -0
- package/lib/test/unit/ios-simulator-session-teardown.spec.js +158 -0
- package/lib/test/unit/logcat-ws.spec.js +78 -37
- package/lib/test/unit/logger-json-levels.spec.js +44 -0
- package/lib/test/unit/mcp-token-ttl.spec.js +38 -0
- package/lib/test/unit/observability-example-loki.spec.js +25 -0
- package/lib/test/unit/openapi-for-site.spec.js +80 -0
- package/lib/test/unit/outbound-proxy.spec.js +320 -0
- package/lib/test/unit/port-exhaustion.spec.js +2 -1
- package/lib/test/unit/proxy-option.spec.js +358 -0
- package/lib/test/unit/run-migrations-database.spec.js +807 -0
- package/lib/test/unit/run-migrations-fail-fast.test.js +41 -27
- package/lib/test/unit/run-migrations.test.js +864 -0
- package/lib/test/unit/selector-health-actions.spec.js +3 -1
- package/lib/test/unit/selector-state-service.spec.js +72 -23
- package/lib/test/unit/selector-verification-clean-build.spec.js +1 -1
- package/lib/test/unit/selector-verification-job.spec.js +15 -6
- package/lib/test/unit/selector-verification-missed-regression.spec.js +2 -2
- package/lib/test/unit/selector-visibility-resolver.spec.js +209 -0
- package/lib/test/unit/session-device-logs-ios.spec.js +180 -0
- package/lib/test/unit/session-device-logs.spec.js +11 -2
- package/lib/test/unit/session-end-failure-analysis.spec.js +5 -5
- package/lib/test/unit/session-log-indexes.spec.js +1 -1
- package/lib/test/unit/socket-team-scope.integration.spec.js +749 -0
- package/lib/test/unit/socketServer.scopedEmit.spec.js +237 -5
- package/lib/test/unit/tracing-export-routing.spec.js +137 -0
- package/lib/test/unit/tracing-session-spans.spec.js +337 -0
- package/lib/test/unit/unexpected-shutdown-reason.spec.js +2 -2
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/schema.json +11 -7
- package/scripts/dev/readme-screenshots.js +276 -23
- package/scripts/dev/screenshot-fixtures/logcat.js +157 -0
- package/scripts/dev/screenshot-fixtures/phone-screen.html +469 -0
- package/scripts/dev/screenshot-fixtures/session.js +242 -0
- package/scripts/export-openapi.js +32 -0
- package/scripts/lib/openapi-for-site.js +79 -0
- package/lib/public/assets/index-BOxe43p1.js +0 -219
- package/lib/public/assets/session-detail-page-AZJE6XXE.js +0 -4
- package/lib/src/scripts/prepare-prisma.js +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenon-device-management/xenon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"description": "Xenon - Intelligent Mobile Infrastructure. A self-healing device orchestration platform for Appium.",
|
|
5
5
|
"main": "./lib/src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"build": "node scripts/generate-prisma.js && tsc -b && npm run build:copy",
|
|
14
14
|
"build:all": "npm run build:xenon && npm run build",
|
|
15
15
|
"build:copy": "rm -rf lib/public && mkdir -p lib/src && cp -R src/public lib/ && cp -R src/generated lib/src/ && cp schema.json lib/ && mkdir -p lib/src/app/openapi && cp src/app/openapi/*.yaml lib/src/app/openapi/ && mkdir -p lib/src/device-managers/android/vendor && cp src/device-managers/android/vendor/*.jar lib/src/device-managers/android/vendor/ && mkdir -p lib/src/services/recording/vendor && cp src/services/recording/vendor/*.ttf src/services/recording/vendor/*.txt lib/src/services/recording/vendor/ && mkdir -p lib/src/services/ocr/vendor && cp src/services/ocr/vendor/*.gz src/services/ocr/vendor/*.txt lib/src/services/ocr/vendor/",
|
|
16
|
+
"build:openapi": "node scripts/export-openapi.js",
|
|
16
17
|
"build:schema": "node scripts/generate-types-from-schema.js",
|
|
17
18
|
"build:xenon": "sh scripts/build-xenon.sh",
|
|
18
19
|
"dev": "npm run db:migrate && npm run build && npm run plugin:reinstall && npm run server",
|
package/schema.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
},
|
|
92
92
|
"proxy": {
|
|
93
93
|
"type": "object",
|
|
94
|
-
"description": "
|
|
94
|
+
"description": "The proxy for every call this server makes to another Xenon server or a cloud provider: a hub's session creates, commands, device control, recordings and live-preview sockets for its nodes' phones, a node's calls to its hub, and Xenon's other internal calls. Hosts that NO_PROXY names, and loopback hosts, go direct. Without it these calls take HTTP_PROXY or HTTPS_PROXY. Webhooks, the AI providers and OpenTelemetry export don't use it. See AxiosProxy interface for details."
|
|
95
95
|
},
|
|
96
96
|
"deviceAvailabilityTimeoutMs": {
|
|
97
97
|
"type": "number",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"sqlite",
|
|
164
164
|
"postgresql"
|
|
165
165
|
],
|
|
166
|
-
"description": "
|
|
166
|
+
"description": "Has no effect, and is accepted only so older configs still start. Xenon stores its data in SQLite, one database per server, hub or node (see databaseUrl). At startup the database itself decides how its tables are brought up to date: from its migration history if that history matches its tables (Xenon first corrects a history that has fallen behind them), otherwise by matching them to this version's schema."
|
|
167
167
|
},
|
|
168
168
|
"databaseUrl": {
|
|
169
169
|
"type": "string",
|
|
@@ -294,24 +294,28 @@
|
|
|
294
294
|
"title": "AxiosProxy",
|
|
295
295
|
"properties": {
|
|
296
296
|
"host": {
|
|
297
|
-
"type": "string"
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": "The proxy's host name or address. Without it the option is ignored, with a warning."
|
|
298
299
|
},
|
|
299
300
|
"port": {
|
|
300
|
-
"type": "integer"
|
|
301
|
+
"type": "integer",
|
|
302
|
+
"description": "The proxy's port."
|
|
301
303
|
},
|
|
302
304
|
"protocol": {
|
|
303
305
|
"type": "string",
|
|
304
306
|
"enum": [
|
|
305
307
|
"http",
|
|
306
308
|
"https"
|
|
307
|
-
]
|
|
309
|
+
],
|
|
310
|
+
"description": "How Xenon talks to the proxy itself; http when not given."
|
|
308
311
|
},
|
|
309
312
|
"auth": {
|
|
310
|
-
"type": "object"
|
|
313
|
+
"type": "object",
|
|
314
|
+
"description": "The proxy's credentials, as { \"username\": ..., \"password\": ... }, sent to it as Proxy-Authorization."
|
|
311
315
|
}
|
|
312
316
|
},
|
|
313
317
|
"additionalProperties": true,
|
|
314
|
-
"description": "
|
|
318
|
+
"description": "A proxy: its host, port, protocol (http or https; http when not given) and auth ({ username, password }), for example { \"host\": \"squid.lab\", \"port\": 3128 }."
|
|
315
319
|
},
|
|
316
320
|
"CloudConfig": {
|
|
317
321
|
"type": "object",
|
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Takes the
|
|
4
|
-
*
|
|
3
|
+
* Takes the dashboard screenshots the README and the documentation site show.
|
|
4
|
+
*
|
|
5
|
+
* README: assets/dashboard-{dark,light}.png (the Devices page)
|
|
6
|
+
* Site: website/static/img/screens/{devices,device-control,session}-{dark,light}.webp
|
|
7
|
+
* (the Devices page, a phone's Logs tab in device control, and a
|
|
8
|
+
* finished session's page), 1600 px wide
|
|
5
9
|
*
|
|
6
10
|
* Serves the built dashboard (web/build) at /xenon/ and answers its API calls
|
|
7
|
-
* with sample
|
|
8
|
-
* pictures are the same on every machine.
|
|
11
|
+
* with sample data, so no server, database or phone is needed and the
|
|
12
|
+
* pictures are the same on every machine. The sample phone's screen is
|
|
13
|
+
* screenshot-fixtures/phone-screen.html: the live preview shows it, and a
|
|
14
|
+
* two-second recording made from it plays in the session's page. The sample
|
|
15
|
+
* session and log lines are in screenshot-fixtures/.
|
|
16
|
+
*
|
|
17
|
+
* The site's pictures are captured at 2x (2880 px wide) and shrunk to 1600 px
|
|
18
|
+
* WebP, which is what a 1200 px column needs and a sixth of the weight. The
|
|
19
|
+
* bundled ffmpeg has no WebP encoder, so Chromium does it: the capture is drawn
|
|
20
|
+
* to a canvas and read back as WebP (see toWebp).
|
|
9
21
|
*
|
|
10
22
|
* cd web && npm run build && cd ..
|
|
11
23
|
* node scripts/dev/readme-screenshots.js
|
|
12
24
|
*
|
|
13
|
-
* Uses web/'s Playwright
|
|
25
|
+
* Uses web/'s Playwright and the bundled ffmpeg (for the recording). Set
|
|
26
|
+
* CHROME_BIN to use a particular Chromium.
|
|
14
27
|
*/
|
|
15
28
|
/* eslint-disable @typescript-eslint/no-var-requires -- a plain Node script, run without a build */
|
|
16
|
-
/* global document, localStorage -- used inside the page, by Playwright */
|
|
29
|
+
/* global document, Image, localStorage -- used inside the page, by Playwright */
|
|
30
|
+
const { execFileSync } = require('child_process');
|
|
17
31
|
const fs = require('fs');
|
|
18
32
|
const http = require('http');
|
|
33
|
+
const os = require('os');
|
|
19
34
|
const path = require('path');
|
|
20
35
|
|
|
21
36
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
22
37
|
const BUILD = path.join(ROOT, 'web', 'build');
|
|
23
38
|
const ASSETS = path.join(ROOT, 'assets');
|
|
39
|
+
const SCREENS = path.join(ROOT, 'website', 'static', 'img', 'screens');
|
|
40
|
+
const FIXTURES = path.join(__dirname, 'screenshot-fixtures');
|
|
41
|
+
const SITE_IMAGE_WIDTH = 1600;
|
|
42
|
+
const SITE_IMAGE_QUALITY = 0.85;
|
|
24
43
|
const PORT = 5179;
|
|
44
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
25
45
|
|
|
26
46
|
const { chromium } = require(path.join(ROOT, 'web', 'node_modules', 'playwright'));
|
|
47
|
+
const { logcatRecords } = require('./screenshot-fixtures/logcat');
|
|
48
|
+
const sample = require('./screenshot-fixtures/session');
|
|
27
49
|
|
|
28
50
|
if (!fs.existsSync(path.join(BUILD, 'index.html'))) {
|
|
29
51
|
console.error('web/build is missing: run `npm run build` in web/ first.');
|
|
@@ -85,6 +107,9 @@ const DEVICES = [
|
|
|
85
107
|
realDevice: true,
|
|
86
108
|
batteryLevel: 92,
|
|
87
109
|
tags: ['payments'],
|
|
110
|
+
// Device control draws the live preview at this size.
|
|
111
|
+
screenWidth: '1344',
|
|
112
|
+
screenHeight: '2992',
|
|
88
113
|
},
|
|
89
114
|
{
|
|
90
115
|
...base,
|
|
@@ -164,17 +189,101 @@ function answer(apiPath) {
|
|
|
164
189
|
if (apiPath === '/devices' || apiPath.startsWith('/device')) return DEVICES;
|
|
165
190
|
if (apiPath === '/queue/length') return 0;
|
|
166
191
|
if (apiPath === '/queue/summary') return { total: 0, byPlatform: {}, otherCount: 0 };
|
|
167
|
-
if (apiPath === '/queue' || apiPath === '/teams'
|
|
192
|
+
if (apiPath === '/queue' || apiPath === '/teams') return [];
|
|
193
|
+
if (apiPath === '/build') return [sample.BUILD];
|
|
168
194
|
if (apiPath.startsWith('/session')) return [];
|
|
169
195
|
if (apiPath.startsWith('/healing/events')) return { events: [], todayCount: 0 };
|
|
170
196
|
return {};
|
|
171
197
|
}
|
|
172
198
|
|
|
173
|
-
|
|
199
|
+
/** Calls a page made that nothing above answers: listed after each picture, to spot a missing sample. */
|
|
200
|
+
const unanswered = new Set();
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Answers one /xenon/api call, or aborts it. `phone` holds the sample phone's
|
|
204
|
+
* screen as a JPEG and the session's recording as a WebM.
|
|
205
|
+
*/
|
|
206
|
+
async function handleApi(route, phone) {
|
|
207
|
+
const request = route.request();
|
|
208
|
+
const apiPath = new URL(request.url()).pathname.replace('/xenon/api', '');
|
|
209
|
+
const method = request.method();
|
|
210
|
+
|
|
211
|
+
// Device control, for the sample Android phone. The live preview is a still
|
|
212
|
+
// JPEG, which an <img> shows as a stream that has just loaded.
|
|
213
|
+
const control = apiPath.match(/^\/control\/([^/]+)\/(.+)$/);
|
|
214
|
+
if (control && decodeURIComponent(control[1]) === sample.UDID) {
|
|
215
|
+
const action = control[2];
|
|
216
|
+
if (action === 'stream' && method === 'GET') {
|
|
217
|
+
return route.fulfill({ contentType: 'image/jpeg', body: phone.jpeg });
|
|
218
|
+
}
|
|
219
|
+
if (action === 'stream/start') return route.fulfill({ json: { success: true, type: 'mjpeg' } });
|
|
220
|
+
if (action === 'stream/ticket')
|
|
221
|
+
return route.fulfill({ json: { ticket: 'sample', expiresIn: 60 } });
|
|
222
|
+
if (action === 'stream/leave') return route.fulfill({ json: {} });
|
|
223
|
+
if (action === 'display') return route.fulfill({ json: { state: 'on' } });
|
|
224
|
+
if (action.startsWith('stream')) return route.abort();
|
|
225
|
+
} else if (apiPath.includes('/stream')) {
|
|
226
|
+
return route.abort();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The sample session's page.
|
|
230
|
+
const session = apiPath.match(/^\/session\/([^/]+)(?:\/(.+))?$/);
|
|
231
|
+
if (session && session[1] === sample.SESSION_ID) {
|
|
232
|
+
const part = session[2] ?? '';
|
|
233
|
+
if (part === '') return route.fulfill({ json: sample.SESSION });
|
|
234
|
+
if (part === 'session_log') return route.fulfill({ json: sample.COMMANDS });
|
|
235
|
+
if (part === 'logs/device') return route.fulfill({ json: sample.DEVICE_LOGS });
|
|
236
|
+
if (part === 'logs/debug') return route.fulfill({ json: sample.DEBUG_LOGS });
|
|
237
|
+
if (part === 'profiling') return route.fulfill({ json: [] });
|
|
238
|
+
if (part === 'metrics') return route.fulfill({ json: sample.METRICS });
|
|
239
|
+
if (part === `asset/video/${sample.VIDEO_FILE}`) {
|
|
240
|
+
return route.fulfill({ contentType: 'video/webm', body: phone.webm });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// No network capture for it: the page says so.
|
|
244
|
+
if (apiPath === `/interceptor/sessions/${sample.SESSION_ID}/requests`) {
|
|
245
|
+
return route.fulfill({ status: 404, json: { error: 'No capture for this session' } });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body = answer(apiPath);
|
|
249
|
+
if (JSON.stringify(body) === '{}') unanswered.add(`${method} ${apiPath}`);
|
|
250
|
+
return route.fulfill({ json: body });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The sample phone's screen, as the live preview's JPEG and as a 2-second
|
|
255
|
+
* WebM (VP9, which the bundled ffmpeg encodes and every Chromium plays).
|
|
256
|
+
*/
|
|
257
|
+
async function preparePhone(browser, workDir) {
|
|
258
|
+
const page = await browser.newPage({ viewport: { width: 1344, height: 2992 } });
|
|
259
|
+
await page.setContent(fs.readFileSync(path.join(FIXTURES, 'phone-screen.html'), 'utf8'));
|
|
260
|
+
const jpeg = await page.screenshot({ type: 'jpeg', quality: 85 });
|
|
261
|
+
await page.close();
|
|
262
|
+
|
|
263
|
+
const still = path.join(workDir, 'phone.jpg');
|
|
264
|
+
const video = path.join(workDir, sample.VIDEO_FILE);
|
|
265
|
+
fs.writeFileSync(still, jpeg);
|
|
266
|
+
execFileSync(
|
|
267
|
+
require(path.join(ROOT, 'node_modules', '@ffmpeg-installer', 'ffmpeg')).path,
|
|
268
|
+
[
|
|
269
|
+
...['-y', '-loglevel', 'error', '-loop', '1', '-framerate', '10', '-i', still, '-t', '2'],
|
|
270
|
+
...['-vf', 'scale=448:996', '-c:v', 'libvpx-vp9', '-b:v', '0', '-crf', '40'],
|
|
271
|
+
...['-pix_fmt', 'yuv420p', '-deadline', 'realtime', '-cpu-used', '8', video],
|
|
272
|
+
],
|
|
273
|
+
{ stdio: 'inherit' },
|
|
274
|
+
);
|
|
275
|
+
return { jpeg, webm: fs.readFileSync(video) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A browser context in the theme, its page answering the dashboard's calls from the samples. */
|
|
279
|
+
async function openPage(browser, theme, viewport, phone) {
|
|
174
280
|
const context = await browser.newContext({
|
|
175
|
-
viewport
|
|
281
|
+
viewport,
|
|
176
282
|
deviceScaleFactor: 2,
|
|
177
283
|
colorScheme: theme,
|
|
284
|
+
// Log lines and sessions show local times: fix them.
|
|
285
|
+
timezoneId: 'UTC',
|
|
286
|
+
locale: 'en-US',
|
|
178
287
|
});
|
|
179
288
|
await context.addInitScript((t) => {
|
|
180
289
|
try {
|
|
@@ -185,17 +294,15 @@ async function shoot(browser, theme, file) {
|
|
|
185
294
|
}, theme);
|
|
186
295
|
const page = await context.newPage();
|
|
187
296
|
await page.route('**/socket.io/**', (route) => route.abort());
|
|
188
|
-
await page.route('**/xenon/api/**', (route) =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return route.fulfill({ json: answer(apiPath) });
|
|
192
|
-
});
|
|
297
|
+
await page.route('**/xenon/api/**', (route) => handleApi(route, phone));
|
|
298
|
+
return { context, page };
|
|
299
|
+
}
|
|
193
300
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
301
|
+
/**
|
|
302
|
+
* The live-updates badge says "Connecting" here only because the socket is
|
|
303
|
+
* blocked; hide it rather than picture a connection problem.
|
|
304
|
+
*/
|
|
305
|
+
async function hideConnectingBadge(page) {
|
|
199
306
|
await page.evaluate(() => {
|
|
200
307
|
for (const el of document.querySelectorAll('header *')) {
|
|
201
308
|
if (el.children.length === 0 && /^Connecting/.test((el.textContent || '').trim())) {
|
|
@@ -210,20 +317,166 @@ async function shoot(browser, theme, file) {
|
|
|
210
317
|
}
|
|
211
318
|
}
|
|
212
319
|
});
|
|
213
|
-
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Throws if any of the texts is on the page: a picture of a spinner or an error is not a picture. */
|
|
323
|
+
async function assertAbsent(page, what, texts) {
|
|
324
|
+
for (const text of texts) {
|
|
325
|
+
if ((await page.getByText(text).count()) > 0) {
|
|
326
|
+
throw new Error(`${what}: the page still says "${text}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Where a picture goes. */
|
|
332
|
+
function save(buffer, ...files) {
|
|
333
|
+
for (const file of files) {
|
|
334
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
335
|
+
fs.writeFileSync(file, buffer);
|
|
336
|
+
console.log(path.relative(ROOT, file));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* A PNG as WebP, `width` px wide (its height in proportion). Chromium encodes
|
|
342
|
+
* it: the PNG is drawn to a canvas of that size and read back as WebP.
|
|
343
|
+
*/
|
|
344
|
+
async function toWebp(browser, png, width = SITE_IMAGE_WIDTH, quality = SITE_IMAGE_QUALITY) {
|
|
345
|
+
const page = await browser.newPage();
|
|
346
|
+
try {
|
|
347
|
+
const dataUrl = await page.evaluate(
|
|
348
|
+
async ({ base64, size, q }) => {
|
|
349
|
+
const image = new Image();
|
|
350
|
+
image.src = `data:image/png;base64,${base64}`;
|
|
351
|
+
await image.decode();
|
|
352
|
+
const canvas = document.createElement('canvas');
|
|
353
|
+
canvas.width = size;
|
|
354
|
+
canvas.height = Math.round((image.naturalHeight * size) / image.naturalWidth);
|
|
355
|
+
const context = canvas.getContext('2d');
|
|
356
|
+
context.imageSmoothingQuality = 'high';
|
|
357
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
358
|
+
return canvas.toDataURL('image/webp', q);
|
|
359
|
+
},
|
|
360
|
+
{ base64: png.toString('base64'), size: width, q: quality },
|
|
361
|
+
);
|
|
362
|
+
if (!dataUrl.startsWith('data:image/webp;base64,')) {
|
|
363
|
+
throw new Error('this Chromium cannot encode WebP');
|
|
364
|
+
}
|
|
365
|
+
return Buffer.from(dataUrl.slice(dataUrl.indexOf(',') + 1), 'base64');
|
|
366
|
+
} finally {
|
|
367
|
+
await page.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The site's copy of a capture: website/static/img/screens/<name>.webp. */
|
|
372
|
+
async function saveForSite(browser, png, name) {
|
|
373
|
+
save(await toWebp(browser, png), path.join(SCREENS, `${name}.webp`));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function reportUnanswered(what) {
|
|
377
|
+
if (unanswered.size > 0) {
|
|
378
|
+
console.log(` ${what}: no sample for ${Array.from(unanswered).join(', ')}`);
|
|
379
|
+
unanswered.clear();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The Devices page: the README's picture and the site's. */
|
|
384
|
+
async function shootDevices(browser, theme, phone) {
|
|
385
|
+
const { context, page } = await openPage(browser, theme, { width: 1440, height: 700 }, phone);
|
|
386
|
+
await page.goto(`${BASE}/xenon/devices`, { waitUntil: 'load' });
|
|
387
|
+
await page.waitForSelector('text=Pixel 8 Pro');
|
|
388
|
+
await page.waitForTimeout(1000);
|
|
389
|
+
await hideConnectingBadge(page);
|
|
390
|
+
const png = await page.screenshot();
|
|
391
|
+
save(png, path.join(ASSETS, `dashboard-${theme}.png`));
|
|
392
|
+
await saveForSite(browser, png, `devices-${theme}`);
|
|
393
|
+
reportUnanswered('devices');
|
|
394
|
+
await context.close();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Device control on the sample Android phone, the Logs tab streaming. */
|
|
398
|
+
async function shootDeviceControl(browser, theme, phone) {
|
|
399
|
+
const { context, page } = await openPage(browser, theme, { width: 1440, height: 900 }, phone);
|
|
400
|
+
// The logs arrive over a WebSocket: send them once it is open, as the
|
|
401
|
+
// phone's logcat does, since the tab clears its lines when the socket opens.
|
|
402
|
+
const live = Date.UTC(2026, 9, 4, 10, 42, 30);
|
|
403
|
+
await page.routeWebSocket(/\/xenon\/api\/control\/[^/]+\/logcat/, (ws) => {
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
for (const record of logcatRecords(live)) ws.send(JSON.stringify(record));
|
|
406
|
+
}, 300);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await page.goto(`${BASE}/xenon/devices/${sample.UDID}/control/logs`, { waitUntil: 'load' });
|
|
410
|
+
await page.waitForFunction(() => {
|
|
411
|
+
const img = document.querySelector('img[alt="Device Stream"]');
|
|
412
|
+
return !!img && img.complete && img.naturalWidth > 0;
|
|
413
|
+
});
|
|
414
|
+
await page.locator('.log-status', { hasText: 'Live' }).waitFor();
|
|
415
|
+
await page.waitForFunction(() => document.querySelectorAll('.log-row').length >= 30);
|
|
416
|
+
await page.waitForTimeout(800);
|
|
417
|
+
await hideConnectingBadge(page);
|
|
418
|
+
await assertAbsent(page, 'device control', [
|
|
419
|
+
'Waiting for stream',
|
|
420
|
+
'ESTABLISHING TRACE',
|
|
421
|
+
'Stream unavailable',
|
|
422
|
+
'Display is off',
|
|
423
|
+
]);
|
|
424
|
+
await saveForSite(
|
|
425
|
+
browser,
|
|
426
|
+
await page.screenshot({ animations: 'disabled' }),
|
|
427
|
+
`device-control-${theme}`,
|
|
428
|
+
);
|
|
429
|
+
reportUnanswered('device control');
|
|
430
|
+
await context.close();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** The page of the sample session: outcome, tiles, healing, CPU and memory, recording. */
|
|
434
|
+
async function shootSession(browser, theme, phone) {
|
|
435
|
+
const { context, page } = await openPage(browser, theme, { width: 1440, height: 900 }, phone);
|
|
436
|
+
await page.goto(`${BASE}/xenon/builds/${sample.BUILD_ID}/sessions/${sample.SESSION_ID}`, {
|
|
437
|
+
waitUntil: 'load',
|
|
438
|
+
});
|
|
439
|
+
await page.getByRole('heading', { name: sample.SESSION.name }).waitFor();
|
|
440
|
+
await page.waitForSelector('section[aria-label="CPU"] svg path');
|
|
441
|
+
await page.waitForFunction(() => {
|
|
442
|
+
const video = document.querySelector('video');
|
|
443
|
+
return !!video && video.readyState >= 2;
|
|
444
|
+
});
|
|
445
|
+
await page.waitForTimeout(800);
|
|
446
|
+
await hideConnectingBadge(page);
|
|
447
|
+
if (!page.url().includes(`/sessions/${sample.SESSION_ID}`)) {
|
|
448
|
+
throw new Error(`session: the page left the session (${page.url()})`);
|
|
449
|
+
}
|
|
450
|
+
await assertAbsent(page, 'session', [
|
|
451
|
+
'Loading session',
|
|
452
|
+
'Loading performance',
|
|
453
|
+
'Session not found',
|
|
454
|
+
'No video available',
|
|
455
|
+
]);
|
|
456
|
+
await saveForSite(
|
|
457
|
+
browser,
|
|
458
|
+
await page.screenshot({ animations: 'disabled' }),
|
|
459
|
+
`session-${theme}`,
|
|
460
|
+
);
|
|
461
|
+
reportUnanswered('session');
|
|
214
462
|
await context.close();
|
|
215
|
-
console.log(`assets/${file}`);
|
|
216
463
|
}
|
|
217
464
|
|
|
218
465
|
(async () => {
|
|
466
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'xenon-screenshots-'));
|
|
219
467
|
await new Promise((resolve) => server.listen(PORT, '127.0.0.1', resolve));
|
|
220
468
|
const browser = await chromium.launch({ executablePath: process.env.CHROME_BIN || undefined });
|
|
221
469
|
try {
|
|
222
|
-
await
|
|
223
|
-
|
|
470
|
+
const phone = await preparePhone(browser, workDir);
|
|
471
|
+
for (const theme of ['dark', 'light']) {
|
|
472
|
+
await shootDevices(browser, theme, phone);
|
|
473
|
+
await shootDeviceControl(browser, theme, phone);
|
|
474
|
+
await shootSession(browser, theme, phone);
|
|
475
|
+
}
|
|
224
476
|
} finally {
|
|
225
477
|
await browser.close();
|
|
226
478
|
server.close();
|
|
479
|
+
fs.rmSync(workDir, { recursive: true, force: true });
|
|
227
480
|
}
|
|
228
481
|
})().catch((err) => {
|
|
229
482
|
console.error(err);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample log lines for the device control screenshot's Logs tab: two minutes
|
|
3
|
+
* of a made-up shop app's checkout on an Android phone. Every record has the
|
|
4
|
+
* shape the logcat WebSocket sends, `{ ts, pid, tid, level, tag, message, pkg }`.
|
|
5
|
+
*
|
|
6
|
+
* About forty lines, mostly info and debug, with the payment's two failed
|
|
7
|
+
* attempts (two errors) and what the app did about them (three warnings).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const APP = 'com.example.shop';
|
|
11
|
+
const SYSTEM = 'system_server';
|
|
12
|
+
const APP_PID = 14233;
|
|
13
|
+
const SYSTEM_PID = 1782;
|
|
14
|
+
|
|
15
|
+
/** [seconds after the first line, level, tag, message] per line, oldest first. */
|
|
16
|
+
const LINES = [
|
|
17
|
+
[
|
|
18
|
+
0.0,
|
|
19
|
+
'I',
|
|
20
|
+
'ActivityTaskManager',
|
|
21
|
+
'START u0 {cmp=com.example.shop/.cart.CartActivity} from uid 10312',
|
|
22
|
+
],
|
|
23
|
+
[0.4, 'D', 'ShopCheckout', 'CartViewModel: 3 items, subtotal=145.50 USD'],
|
|
24
|
+
[1.9, 'D', 'OkHttp', '--> GET https://api.example.com/v2/cart/c_91f2/quote'],
|
|
25
|
+
[
|
|
26
|
+
2.2,
|
|
27
|
+
'D',
|
|
28
|
+
'OkHttp',
|
|
29
|
+
'<-- 200 https://api.example.com/v2/cart/c_91f2/quote (287ms, 1421-byte body)',
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
4.8,
|
|
33
|
+
'I',
|
|
34
|
+
'ActivityTaskManager',
|
|
35
|
+
'START u0 {cmp=com.example.shop/.checkout.CheckoutActivity} from uid 10312',
|
|
36
|
+
],
|
|
37
|
+
[5.1, 'D', 'ShopCheckout', 'CheckoutViewModel init cartId=c_91f2 items=3'],
|
|
38
|
+
[5.3, 'D', 'ShopCheckout', 'loadSavedCards() -> 2 cards'],
|
|
39
|
+
[
|
|
40
|
+
5.7,
|
|
41
|
+
'I',
|
|
42
|
+
'ActivityTaskManager',
|
|
43
|
+
'Displayed com.example.shop/.checkout.CheckoutActivity for user 0: +412ms',
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
6.0,
|
|
47
|
+
'D',
|
|
48
|
+
'chromium',
|
|
49
|
+
'[INFO:CONSOLE(1)] "Payment sheet ready", source: https://pay.example.com/sheet.js (1)',
|
|
50
|
+
],
|
|
51
|
+
[9.6, 'D', 'ShopCheckout', 'promo SPRING10 validated: discount=-14.55'],
|
|
52
|
+
[10.1, 'D', 'OkHttp', '--> POST https://api.example.com/v2/promo/validate (38-byte body)'],
|
|
53
|
+
[10.4, 'D', 'OkHttp', '<-- 200 https://api.example.com/v2/promo/validate (301ms, 96-byte body)'],
|
|
54
|
+
[18.5, 'I', 'ShopCheckout', 'payment method selected: card_visa_4242'],
|
|
55
|
+
[
|
|
56
|
+
24.2,
|
|
57
|
+
'D',
|
|
58
|
+
'chromium',
|
|
59
|
+
'[INFO:CONSOLE(1)] "3DS challenge not required", source: https://pay.example.com/sheet.js (1)',
|
|
60
|
+
],
|
|
61
|
+
[41.0, 'I', 'ShopCheckout', 'Pay now tapped total=130.95 USD'],
|
|
62
|
+
[41.1, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
|
|
63
|
+
[43.2, 'E', 'ShopCheckout', 'payment attempt 1 of 3 failed: SocketTimeoutException: timeout'],
|
|
64
|
+
[43.3, 'W', 'ShopCheckout', 'retrying payment in 1500ms idempotencyKey=pk_7c1e'],
|
|
65
|
+
[44.8, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
|
|
66
|
+
[
|
|
67
|
+
46.3,
|
|
68
|
+
'E',
|
|
69
|
+
'ShopCheckout',
|
|
70
|
+
'payment attempt 2 of 3 failed: HTTP 503 (payments-gateway unavailable)',
|
|
71
|
+
],
|
|
72
|
+
[46.4, 'W', 'ShopCheckout', 'retrying payment in 3000ms idempotencyKey=pk_7c1e'],
|
|
73
|
+
[49.5, 'D', 'OkHttp', '--> POST https://api.example.com/v2/payments (412-byte body)'],
|
|
74
|
+
[50.3, 'D', 'OkHttp', '<-- 201 https://api.example.com/v2/payments (801ms, 214-byte body)'],
|
|
75
|
+
[50.4, 'I', 'ShopCheckout', 'payment captured paymentId=pay_3f9a2c attempt=3'],
|
|
76
|
+
[
|
|
77
|
+
50.6,
|
|
78
|
+
'W',
|
|
79
|
+
'chromium',
|
|
80
|
+
'[WARNING:CONSOLE(1)] "Slow payment: 9.3s from tap to capture", source: https://pay.example.com/sheet.js (1)',
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
50.9,
|
|
84
|
+
'I',
|
|
85
|
+
'ActivityTaskManager',
|
|
86
|
+
'START u0 {cmp=com.example.shop/.order.OrderConfirmationActivity} from uid 10312',
|
|
87
|
+
],
|
|
88
|
+
[51.1, 'D', 'ShopCheckout', 'OrderConfirmationViewModel orderId=o_55820'],
|
|
89
|
+
[
|
|
90
|
+
51.3,
|
|
91
|
+
'I',
|
|
92
|
+
'ActivityTaskManager',
|
|
93
|
+
'Displayed com.example.shop/.order.OrderConfirmationActivity for user 0: +188ms',
|
|
94
|
+
],
|
|
95
|
+
[51.8, 'D', 'OkHttp', '--> POST https://analytics.example.com/v1/events (233-byte body)'],
|
|
96
|
+
[52.1, 'D', 'OkHttp', '<-- 202 https://analytics.example.com/v1/events (264ms, 0-byte body)'],
|
|
97
|
+
[
|
|
98
|
+
60.4,
|
|
99
|
+
'D',
|
|
100
|
+
'chromium',
|
|
101
|
+
'[INFO:CONSOLE(1)] "Receipt rendered", source: https://pay.example.com/receipt.js (1)',
|
|
102
|
+
],
|
|
103
|
+
[63.0, 'I', 'ShopCheckout', 'receipt email queued orderId=o_55820'],
|
|
104
|
+
[63.2, 'D', 'OkHttp', '--> POST https://api.example.com/v2/orders/o_55820/receipt (0-byte body)'],
|
|
105
|
+
[
|
|
106
|
+
63.6,
|
|
107
|
+
'D',
|
|
108
|
+
'OkHttp',
|
|
109
|
+
'<-- 202 https://api.example.com/v2/orders/o_55820/receipt (402ms, 0-byte body)',
|
|
110
|
+
],
|
|
111
|
+
[78.9, 'D', 'ShopCheckout', 'CartRepository: cart c_91f2 cleared'],
|
|
112
|
+
[
|
|
113
|
+
91.5,
|
|
114
|
+
'I',
|
|
115
|
+
'ActivityTaskManager',
|
|
116
|
+
'Displayed com.example.shop/.home.HomeActivity for user 0: +96ms',
|
|
117
|
+
],
|
|
118
|
+
[92.2, 'D', 'OkHttp', '--> GET https://api.example.com/v2/recommendations?limit=12'],
|
|
119
|
+
[
|
|
120
|
+
92.7,
|
|
121
|
+
'D',
|
|
122
|
+
'OkHttp',
|
|
123
|
+
'<-- 200 https://api.example.com/v2/recommendations?limit=12 (512ms, 9.8-kB body)',
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
104.3,
|
|
127
|
+
'D',
|
|
128
|
+
'chromium',
|
|
129
|
+
'[INFO:CONSOLE(1)] "Session heartbeat", source: https://pay.example.com/sheet.js (1)',
|
|
130
|
+
],
|
|
131
|
+
[116.8, 'D', 'ShopCheckout', 'HomeViewModel: refreshed 12 recommendations'],
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The records, the last one `endTs` (epoch ms) and the first about two
|
|
136
|
+
* minutes before it. A line's thread id is its pid plus a fixed offset, so
|
|
137
|
+
* the same input gives the same records every run.
|
|
138
|
+
*/
|
|
139
|
+
function logcatRecords(endTs) {
|
|
140
|
+
const last = LINES[LINES.length - 1][0];
|
|
141
|
+
const start = endTs - Math.round((last + 1.2) * 1000);
|
|
142
|
+
return LINES.map(([offset, level, tag, message], i) => {
|
|
143
|
+
const fromSystem = tag === 'ActivityTaskManager';
|
|
144
|
+
const pid = fromSystem ? SYSTEM_PID : APP_PID;
|
|
145
|
+
return {
|
|
146
|
+
ts: start + Math.round(offset * 1000),
|
|
147
|
+
pid,
|
|
148
|
+
tid: fromSystem ? pid + 449 : pid + (i % 3 === 0 ? 0 : 27),
|
|
149
|
+
level,
|
|
150
|
+
tag,
|
|
151
|
+
message,
|
|
152
|
+
pkg: fromSystem ? SYSTEM : APP,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { logcatRecords, LINES };
|