@expo/build-tools 21.0.2 → 21.1.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/dist/builders/android.js +12 -1
- package/dist/builders/ios.js +13 -1
- package/dist/common/easBuildInternal.js +3 -0
- package/dist/common/hookableBuildPhase.d.ts +18 -0
- package/dist/common/hookableBuildPhase.js +72 -0
- package/dist/common/jobHooks.d.ts +16 -0
- package/dist/common/jobHooks.js +95 -0
- package/dist/common/setup.d.ts +14 -2
- package/dist/common/setup.js +40 -21
- package/dist/index.d.ts +4 -3
- package/dist/index.js +5 -5
- package/dist/{gcs/LoggerStream.d.ts → logging/RemoteLoggerStream.d.ts} +6 -6
- package/dist/{gcs/LoggerStream.js → logging/RemoteLoggerStream.js} +10 -10
- package/dist/steps/functions/maestroResultParser.d.ts +1 -0
- package/dist/steps/functions/maestroResultParser.js +12 -0
- package/dist/steps/functions/maestroScreenshots.d.ts +1 -0
- package/dist/steps/functions/maestroScreenshots.js +167 -34
- package/dist/steps/functions/maestroTests.js +4 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.d.ts +8 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +35 -3
- package/dist/steps/utils/deviceRunSessionEvents.d.ts +23 -0
- package/dist/steps/utils/deviceRunSessionEvents.js +311 -0
- package/dist/{gcs → storage}/retry.d.ts +1 -1
- package/dist/{gcs → storage}/retry.js +2 -2
- package/dist/storage/uploadWithSignedUrl.d.ts +12 -0
- package/dist/{gcs/client.js → storage/uploadWithSignedUrl.js} +27 -31
- package/package.json +4 -4
- package/dist/gcs/client.d.ts +0 -15
|
@@ -9,10 +9,18 @@ exports.computePureFailureFlowNames = computePureFailureFlowNames;
|
|
|
9
9
|
exports.selectFailureScreenshots = selectFailureScreenshots;
|
|
10
10
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
// Maestro
|
|
12
|
+
// pre-v2.7.0 (Maestro <= 2.6.x): failure screenshots are flat files named
|
|
13
|
+
// `screenshot-[shard-N-]❌-<epochMillis>-(<flowName>).png` directly in the session dir.
|
|
13
14
|
// The flow name may contain parentheses, so anchor on the `-(` after the epoch and the
|
|
14
|
-
// `).png` suffix rather than scanning for parens.
|
|
15
|
+
// `).png` suffix rather than scanning for parens. Remove this path once the build fleet is
|
|
16
|
+
// entirely on Maestro >= 2.7.0.
|
|
15
17
|
const FAILURE_SCREENSHOT_PATTERN = /^screenshot-(?:shard-\d+-)?❌-(\d+)-\((.+)\)\.png$/u;
|
|
18
|
+
// Maestro >= 2.7.0 bundle layout. Each flow gets its own dir under the session dir, with step
|
|
19
|
+
// screenshots under `screenshots/` named `step-<NNN>-<slug>.png` (NNN is a 1-based, zero-padded
|
|
20
|
+
// monotonic step sequence). Under the CLI (no `--analyze`) only failed/warned steps produce one.
|
|
21
|
+
const STEP_SCREENSHOTS_DIRNAME = 'screenshots';
|
|
22
|
+
const STEP_SCREENSHOT_PATTERN = /^step-(\d+)(?:-.*)?\.png$/;
|
|
23
|
+
// pre-v2.7.0: parses the legacy flat failure-screenshot filename.
|
|
16
24
|
function parseFailureScreenshotFilename(filename) {
|
|
17
25
|
const match = filename.match(FAILURE_SCREENSHOT_PATTERN);
|
|
18
26
|
if (!match) {
|
|
@@ -27,61 +35,186 @@ function parseFailureScreenshotFilename(filename) {
|
|
|
27
35
|
capturedAtMs,
|
|
28
36
|
};
|
|
29
37
|
}
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
38
|
+
// Harvests one attempt's failure screenshots from the maestro debug output. Walks each session
|
|
39
|
+
// dir under testsDirectory whose mtime is recent enough (mtime-based to also cover the maestro
|
|
40
|
+
// <2.5.0 bug that splits one invocation across two dirs). Within a session dir, a child is either
|
|
41
|
+
// a flat pre-v2.7.0 screenshot file or a Maestro >= 2.7.0 per-flow bundle dir — the single
|
|
42
|
+
// structural seam below. Never throws: an unreadable dir/file is logged and skipped so a
|
|
43
|
+
// screenshot harvest can't affect the maestro step outcome.
|
|
34
44
|
async function harvestFailureScreenshotsAsync(args) {
|
|
35
45
|
// Gate dirs by mtime floored to the second, so a dir created within the same second as the
|
|
36
|
-
// attempt start isn't stat'd below the baseline. The exact capturedAtMs gate
|
|
46
|
+
// attempt start isn't stat'd below the baseline. The exact capturedAtMs gate per file is what
|
|
37
47
|
// prevents mis-attributing an earlier attempt's screenshot.
|
|
38
48
|
const dirSinceMtimeMs = Math.floor(args.capturedSinceMs / 1000) * 1000;
|
|
39
|
-
|
|
49
|
+
// Bundle dir names already have '/' -> '_'; normalize the JUnit names the same way to match.
|
|
50
|
+
const normalizedFailedFlowNames = new Set([...args.failedFlowNames].map(normalizeFlowName));
|
|
51
|
+
let sessionEntries;
|
|
40
52
|
try {
|
|
41
|
-
|
|
53
|
+
sessionEntries = await promises_1.default.readdir(args.testsDirectory, { withFileTypes: true });
|
|
42
54
|
}
|
|
43
55
|
catch (err) {
|
|
44
56
|
args.logger.info({ err }, `Skipping screenshot harvest: cannot read ${args.testsDirectory}.`);
|
|
45
57
|
return [];
|
|
46
58
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
const legacyShots = [];
|
|
60
|
+
const bundleShots = [];
|
|
61
|
+
for (const sessionEntry of sessionEntries) {
|
|
62
|
+
if (!sessionEntry.isDirectory()) {
|
|
50
63
|
continue;
|
|
51
64
|
}
|
|
52
|
-
const
|
|
53
|
-
let
|
|
65
|
+
const sessionDir = path_1.default.join(args.testsDirectory, sessionEntry.name);
|
|
66
|
+
let children;
|
|
54
67
|
try {
|
|
55
|
-
if ((await promises_1.default.stat(
|
|
68
|
+
if ((await promises_1.default.stat(sessionDir)).mtimeMs < dirSinceMtimeMs) {
|
|
56
69
|
continue;
|
|
57
70
|
}
|
|
58
|
-
|
|
71
|
+
children = await promises_1.default.readdir(sessionDir, { withFileTypes: true });
|
|
59
72
|
}
|
|
60
73
|
catch (err) {
|
|
61
|
-
args.logger.info({ err }, `Skipping unreadable debug dir ${
|
|
74
|
+
args.logger.info({ err }, `Skipping unreadable debug dir ${sessionDir}.`);
|
|
62
75
|
continue;
|
|
63
76
|
}
|
|
64
|
-
for (const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
for (const child of children) {
|
|
78
|
+
if (child.isDirectory()) {
|
|
79
|
+
// Maestro >= 2.7.0: a per-flow bundle dir.
|
|
80
|
+
const shot = await harvestFromBundleAsync({
|
|
81
|
+
bundleDir: path_1.default.join(sessionDir, child.name),
|
|
82
|
+
bundleName: child.name,
|
|
83
|
+
normalizedFailedFlowNames,
|
|
84
|
+
capturedSinceMs: args.capturedSinceMs,
|
|
85
|
+
attemptIndex: args.attemptIndex,
|
|
86
|
+
logger: args.logger,
|
|
87
|
+
});
|
|
88
|
+
if (shot !== null) {
|
|
89
|
+
bundleShots.push(shot);
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
92
|
+
else {
|
|
93
|
+
// pre-v2.7.0: a flat failure screenshot file in the session dir.
|
|
94
|
+
const shot = harvestLegacyFlatShot({
|
|
95
|
+
fileName: child.name,
|
|
96
|
+
sessionDir,
|
|
97
|
+
capturedSinceMs: args.capturedSinceMs,
|
|
78
98
|
attemptIndex: args.attemptIndex,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
});
|
|
100
|
+
if (shot !== null) {
|
|
101
|
+
legacyShots.push(shot);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// The website shows one screenshot per (flow, attempt). Two v2.7.0 bundle dirs can resolve to
|
|
107
|
+
// the same flow — a collision dir (`<flow>-2`) or a duplicate flow name — so keep the latest.
|
|
108
|
+
// Legacy shots need no dedupe: a flat filename is emitted once per failed flow, so there is no
|
|
109
|
+
// `<flow>-2` collision to collapse (and deduping them would change historical behavior).
|
|
110
|
+
return [...legacyShots, ...dedupeBundleShotsByFlowName(bundleShots)];
|
|
111
|
+
}
|
|
112
|
+
// Maestro >= 2.7.0: resolve a bundle dir to its owning failed flow and return that flow's failure
|
|
113
|
+
// screenshot — the highest-numbered step in `screenshots/`. A required failure halts the flow, so
|
|
114
|
+
// the failed step is normally the last (highest-numbered) captured step; earlier warned steps have
|
|
115
|
+
// lower numbers. Best-effort limitation: without reading Maestro's per-command JSON we can't tell a
|
|
116
|
+
// failed-step frame from a warned-step frame, so if the failing command captured nothing (e.g. a
|
|
117
|
+
// non-visible command like runScript) but an earlier optional step warned, we return that warned
|
|
118
|
+
// frame instead. Returns null when the bundle isn't a failed flow, its name is ambiguous, or it has
|
|
119
|
+
// no usable step screenshot. Never throws.
|
|
120
|
+
async function harvestFromBundleAsync(args) {
|
|
121
|
+
const flowName = resolveBundleFlowName(args.bundleName, args.normalizedFailedFlowNames);
|
|
122
|
+
if (flowName === null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const screenshotsDir = path_1.default.join(args.bundleDir, STEP_SCREENSHOTS_DIRNAME);
|
|
126
|
+
let filenames;
|
|
127
|
+
try {
|
|
128
|
+
filenames = await promises_1.default.readdir(screenshotsDir);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// No screenshots/ dir — e.g. the flow failed on a non-visible command, which captures none.
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
let best = null;
|
|
135
|
+
for (const filename of filenames) {
|
|
136
|
+
const match = filename.match(STEP_SCREENSHOT_PATTERN);
|
|
137
|
+
if (match === null) {
|
|
138
|
+
continue; // final.png and any non-step file
|
|
139
|
+
}
|
|
140
|
+
const stepIndex = Number(match[1]);
|
|
141
|
+
const fileAbsPath = path_1.default.join(screenshotsDir, filename);
|
|
142
|
+
let capturedAtMs;
|
|
143
|
+
try {
|
|
144
|
+
capturedAtMs = Math.round((await promises_1.default.stat(fileAbsPath)).mtimeMs);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
args.logger.info({ err }, `Skipping unreadable screenshot ${fileAbsPath}.`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (capturedAtMs < args.capturedSinceMs) {
|
|
151
|
+
continue; // a prior attempt's frame (bundle dir touched later than this attempt started)
|
|
152
|
+
}
|
|
153
|
+
if (best === null || stepIndex > best.stepIndex) {
|
|
154
|
+
best = { fileAbsPath, stepIndex, capturedAtMs };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (best === null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
fileAbsPath: best.fileAbsPath,
|
|
162
|
+
displayName: `Failure Screenshot: ${flowName} (attempt ${args.attemptIndex + 1})`,
|
|
163
|
+
metadata: {
|
|
164
|
+
kind: 'maestro-test-screenshot',
|
|
165
|
+
flowName,
|
|
166
|
+
attemptIndex: args.attemptIndex,
|
|
167
|
+
capturedAtMs: best.capturedAtMs,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Maestro names a bundle dir `<cleanFlow>` optionally + `-shard-<n>` (sharded) + `-<n>` (name
|
|
172
|
+
// collision), where cleanFlow is the flow name with '/' -> '_'. Those suffixes share a namespace
|
|
173
|
+
// with legitimate flow names, so we can't strip blindly (a flow may be named `x-shard-1` or `x-2`).
|
|
174
|
+
// Enumerate the ways the dir name could peel back to a flow name, keep those that match a failed
|
|
175
|
+
// flow, and use the result only if exactly one fits — otherwise skip rather than guess.
|
|
176
|
+
function resolveBundleFlowName(bundleName, normalizedFailedFlowNames) {
|
|
177
|
+
const withoutCollision = bundleName.replace(/-\d+$/, '');
|
|
178
|
+
const preimages = [
|
|
179
|
+
bundleName,
|
|
180
|
+
withoutCollision,
|
|
181
|
+
withoutCollision.replace(/-shard-\d+$/, ''),
|
|
182
|
+
bundleName.replace(/-shard-\d+$/, ''),
|
|
183
|
+
];
|
|
184
|
+
const matches = new Set(preimages.filter(name => normalizedFailedFlowNames.has(name)));
|
|
185
|
+
return matches.size === 1 ? [...matches][0] : null;
|
|
186
|
+
}
|
|
187
|
+
// Keep one screenshot per flow name (attemptIndex is constant within a harvest call), preferring
|
|
188
|
+
// the latest capture when two bundles resolve to the same flow.
|
|
189
|
+
function dedupeBundleShotsByFlowName(shots) {
|
|
190
|
+
const byFlowName = new Map();
|
|
191
|
+
for (const shot of shots) {
|
|
192
|
+
const existing = byFlowName.get(shot.metadata.flowName);
|
|
193
|
+
if (existing === undefined || shot.metadata.capturedAtMs > existing.metadata.capturedAtMs) {
|
|
194
|
+
byFlowName.set(shot.metadata.flowName, shot);
|
|
82
195
|
}
|
|
83
196
|
}
|
|
84
|
-
return
|
|
197
|
+
return [...byFlowName.values()];
|
|
198
|
+
}
|
|
199
|
+
// pre-v2.7.0 (Maestro <= 2.6.x) reader: a flat `screenshot-...❌...png` file whose name carries the
|
|
200
|
+
// flow name and capture epoch.
|
|
201
|
+
function harvestLegacyFlatShot(args) {
|
|
202
|
+
const parsed = parseFailureScreenshotFilename(args.fileName);
|
|
203
|
+
// Re-check capture time per file — a dir's mtime can advance after this attempt began, so the dir
|
|
204
|
+
// gate alone would re-attribute an earlier attempt's screenshot to this one.
|
|
205
|
+
if (parsed === null || parsed.capturedAtMs < args.capturedSinceMs) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
fileAbsPath: path_1.default.join(args.sessionDir, args.fileName),
|
|
210
|
+
displayName: `Failure Screenshot: ${parsed.flowName} (attempt ${args.attemptIndex + 1})`,
|
|
211
|
+
metadata: {
|
|
212
|
+
kind: 'maestro-test-screenshot',
|
|
213
|
+
flowName: parsed.flowName,
|
|
214
|
+
attemptIndex: args.attemptIndex,
|
|
215
|
+
capturedAtMs: parsed.capturedAtMs,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
85
218
|
}
|
|
86
219
|
// Maestro substitutes '/' -> '_' in screenshot filenames (so HarvestedScreenshot.flowName
|
|
87
220
|
// is already in that form) but keeps '/' in the JUnit testcase name. Normalize the JUnit
|
|
@@ -216,10 +216,14 @@ function createMaestroTestsBuildFunction(ctx) {
|
|
|
216
216
|
// junit: test-case-result rows (and therefore the summary icons) only exist for junit
|
|
217
217
|
// runs, so harvesting other formats would just create orphan artifacts the website hides.
|
|
218
218
|
if (outputFormat === 'junit') {
|
|
219
|
+
const failedFlowNames = outputPath
|
|
220
|
+
? await (0, maestroResultParser_1.parseFailedFlowNamesFromJUnitFile)(outputPath)
|
|
221
|
+
: new Set();
|
|
219
222
|
harvested.push(...(await (0, maestroScreenshots_1.harvestFailureScreenshotsAsync)({
|
|
220
223
|
testsDirectory,
|
|
221
224
|
capturedSinceMs: attemptStartedAtMs,
|
|
222
225
|
attemptIndex: attempt,
|
|
226
|
+
failedFlowNames,
|
|
223
227
|
logger,
|
|
224
228
|
})));
|
|
225
229
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { type bunyan } from '@expo/logger';
|
|
1
2
|
import { BuildFunction } from '@expo/steps';
|
|
2
3
|
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
4
|
export declare function createStartAgentDeviceRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
|
|
5
|
+
export declare function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }: {
|
|
6
|
+
eventCollection: {
|
|
7
|
+
stopAsync: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
deviceRunSessionId: string;
|
|
10
|
+
logger: bunyan;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDeviceRemoteSessionBuildFunction;
|
|
7
|
+
exports.stopAgentDeviceEventCollectionSafelyAsync = stopAgentDeviceEventCollectionSafelyAsync;
|
|
7
8
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
9
|
const steps_1 = require("@expo/steps");
|
|
9
10
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
@@ -12,11 +13,13 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
12
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
14
|
const sentry_1 = require("../../sentry");
|
|
14
15
|
const agentDeviceArtifacts_1 = require("../utils/agentDeviceArtifacts");
|
|
16
|
+
const deviceRunSessionEvents_1 = require("../utils/deviceRunSessionEvents");
|
|
15
17
|
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
16
18
|
const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
|
|
17
19
|
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstack/agent-device.git';
|
|
18
20
|
const SRC_DIR = '/tmp/agent-device-src';
|
|
19
|
-
const
|
|
21
|
+
const AGENT_DEVICE_STATE_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device');
|
|
22
|
+
const DAEMON_JSON_PATH = node_path_1.default.join(AGENT_DEVICE_STATE_DIR, 'daemon.json');
|
|
20
23
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
21
24
|
const AGENT_DEVICE_DAEMON_ENV = {
|
|
22
25
|
AGENT_DEVICE_DAEMON_SERVER_MODE: 'http',
|
|
@@ -93,15 +96,44 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
93
96
|
daemonToken,
|
|
94
97
|
logger,
|
|
95
98
|
});
|
|
96
|
-
await (0,
|
|
99
|
+
const eventCollection = await (0, deviceRunSessionEvents_1.startAgentDeviceEventCollectionAsync)({
|
|
97
100
|
ctx,
|
|
98
101
|
deviceRunSessionId,
|
|
102
|
+
stateDir: AGENT_DEVICE_STATE_DIR,
|
|
99
103
|
logger,
|
|
100
|
-
signal,
|
|
101
104
|
});
|
|
105
|
+
try {
|
|
106
|
+
await (0, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
|
|
107
|
+
ctx,
|
|
108
|
+
deviceRunSessionId,
|
|
109
|
+
logger,
|
|
110
|
+
signal,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
await stopAgentDeviceEventCollectionSafelyAsync({
|
|
115
|
+
eventCollection,
|
|
116
|
+
deviceRunSessionId,
|
|
117
|
+
logger,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
102
120
|
},
|
|
103
121
|
});
|
|
104
122
|
}
|
|
123
|
+
async function stopAgentDeviceEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }) {
|
|
124
|
+
try {
|
|
125
|
+
await eventCollection.stopAsync();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
129
|
+
sentry_1.Sentry.capture('Could not finish agent-device session event collection', error, {
|
|
130
|
+
level: 'warning',
|
|
131
|
+
tags: { phase: 'agent-device-event-collection', operation: 'stop' },
|
|
132
|
+
extras: { deviceRunSessionId },
|
|
133
|
+
});
|
|
134
|
+
logger.warn({ err: error }, 'Could not finish agent-device session event collection.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
105
137
|
async function startAgentDeviceDaemonAsync({ packageVersion, env, logger, }) {
|
|
106
138
|
const packageSpec = createAgentDevicePackageSpec(packageVersion);
|
|
107
139
|
try {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type bunyan } from '@expo/logger';
|
|
2
|
+
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
export type DeviceRunSessionEvent = {
|
|
4
|
+
v: 1;
|
|
5
|
+
eventId: string;
|
|
6
|
+
ts: string;
|
|
7
|
+
producer: string;
|
|
8
|
+
type: string;
|
|
9
|
+
operationId?: string;
|
|
10
|
+
outcome?: 'success' | 'failure';
|
|
11
|
+
durationMs?: number;
|
|
12
|
+
summary: string;
|
|
13
|
+
data?: Record<string, unknown>;
|
|
14
|
+
};
|
|
15
|
+
export declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
|
|
16
|
+
ctx: CustomBuildContext;
|
|
17
|
+
deviceRunSessionId: string;
|
|
18
|
+
stateDir: string;
|
|
19
|
+
logger: bunyan;
|
|
20
|
+
pollIntervalMs?: number;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
stopAsync: () => Promise<void>;
|
|
23
|
+
}>;
|