@expo/build-tools 21.3.0 → 21.5.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/android/gradleProfile.js +14 -2
- package/dist/builders/custom.js +20 -12
- package/dist/common/git.d.ts +4 -0
- package/dist/common/git.js +35 -2
- package/dist/common/jobHooks.js +10 -0
- package/dist/generic.js +15 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/logging/HttpLogStream.d.ts +28 -0
- package/dist/logging/HttpLogStream.js +137 -0
- package/dist/steps/compositeFunctions.d.ts +11 -0
- package/dist/steps/compositeFunctions.js +62 -0
- package/dist/steps/easFunctions.js +4 -0
- package/dist/steps/functions/checkout.js +26 -1
- package/dist/steps/functions/collectServeSimMetrics.d.ts +3 -0
- package/dist/steps/functions/collectServeSimMetrics.js +41 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +49 -41
- package/dist/steps/functions/startArgentRemoteSession.d.ts +8 -1
- package/dist/steps/functions/startArgentRemoteSession.js +52 -4
- package/dist/steps/functions/startServeSimMetrics.d.ts +2 -0
- package/dist/steps/functions/startServeSimMetrics.js +17 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +19 -14
- package/dist/steps/utils/agentDeviceArtifacts.js +1 -1
- package/dist/steps/utils/agentDeviceEvents.d.ts +11 -0
- package/dist/steps/utils/agentDeviceEvents.js +116 -0
- package/dist/steps/utils/argentArtifacts.js +1 -1
- package/dist/steps/utils/argentEvents.d.ts +12 -0
- package/dist/steps/utils/argentEvents.js +131 -0
- package/dist/steps/utils/deviceRunSessionEvents.d.ts +43 -2
- package/dist/steps/utils/deviceRunSessionEvents.js +95 -115
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +43 -5
- package/dist/steps/utils/remoteDeviceRunSession.js +249 -37
- package/dist/steps/utils/serveSimMetricsArtifacts.d.ts +9 -0
- package/dist/steps/utils/serveSimMetricsArtifacts.js +49 -0
- package/dist/steps/utils/serveSimMetricsRecorder.d.ts +30 -0
- package/dist/steps/utils/serveSimMetricsRecorder.js +232 -0
- package/package.json +4 -4
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type bunyan } from '@expo/logger';
|
|
2
2
|
import { type CustomBuildContext } from '../../customBuildContext';
|
|
3
|
+
/**
|
|
4
|
+
* The normalized, producer-agnostic event shape uploaded to the API server.
|
|
5
|
+
* Every remote-session producer (agent-device, argent, ...) tails its own
|
|
6
|
+
* event log and maps its records onto this common contract so consumers render
|
|
7
|
+
* a single unified session timeline.
|
|
8
|
+
*/
|
|
3
9
|
export type DeviceRunSessionEvent = {
|
|
4
10
|
v: 1;
|
|
5
11
|
eventId: string;
|
|
@@ -12,10 +18,45 @@ export type DeviceRunSessionEvent = {
|
|
|
12
18
|
summary: string;
|
|
13
19
|
data?: Record<string, unknown>;
|
|
14
20
|
};
|
|
15
|
-
export
|
|
21
|
+
export type DeviceRunSessionEventParseFailure = 'invalid-json' | 'invalid-event';
|
|
22
|
+
export type DeviceRunSessionEventParseResult = {
|
|
23
|
+
event?: DeviceRunSessionEvent;
|
|
24
|
+
failure?: DeviceRunSessionEventParseFailure;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Producer-specific adapter plugged into the generic collection engine. It only
|
|
28
|
+
* has to say where its event files live and how to turn one raw NDJSON line
|
|
29
|
+
* into a {@link DeviceRunSessionEvent}; the engine owns tailing, upload, polling
|
|
30
|
+
* and failure reporting.
|
|
31
|
+
*/
|
|
32
|
+
export type DeviceRunSessionEventSource = {
|
|
33
|
+
/**
|
|
34
|
+
* Stable producer identifier, e.g. `agent-device` or `argent`. Also used
|
|
35
|
+
* verbatim in diagnostic messages, which are phrased so it never needs
|
|
36
|
+
* recasing.
|
|
37
|
+
*/
|
|
38
|
+
producer: string;
|
|
39
|
+
/** Discover the NDJSON event files to tail (absolute paths). */
|
|
40
|
+
findEventFilesAsync: () => Promise<string[]>;
|
|
41
|
+
/** Namespace component of the event ID derived from a tailed file path. */
|
|
42
|
+
sourceKeyForFile: (eventFile: string) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Parse one raw NDJSON line. `sequenceNumber` is monotonic per file (kept
|
|
45
|
+
* stable across truncations) and `sourceKey` namespaces the event ID so an ID
|
|
46
|
+
* is never reused during collection. Blank lines should return an empty
|
|
47
|
+
* result so they neither emit an event nor count as a parse failure.
|
|
48
|
+
*/
|
|
49
|
+
parseLine: (args: {
|
|
50
|
+
line: string;
|
|
51
|
+
sourceKey: string;
|
|
52
|
+
sequenceNumber: number;
|
|
53
|
+
deviceRunSessionId: string;
|
|
54
|
+
}) => DeviceRunSessionEventParseResult;
|
|
55
|
+
};
|
|
56
|
+
export declare function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs, }: {
|
|
16
57
|
ctx: CustomBuildContext;
|
|
17
58
|
deviceRunSessionId: string;
|
|
18
|
-
|
|
59
|
+
source: DeviceRunSessionEventSource;
|
|
19
60
|
logger: bunyan;
|
|
20
61
|
pollIntervalMs?: number;
|
|
21
62
|
}): Promise<{
|
|
@@ -3,36 +3,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.startDeviceRunSessionEventCollectionAsync = startDeviceRunSessionEventCollectionAsync;
|
|
7
7
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
8
|
const gql_tada_1 = require("gql.tada");
|
|
9
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
10
|
const node_string_decoder_1 = require("node:string_decoder");
|
|
12
11
|
const promises_1 = require("node:timers/promises");
|
|
13
|
-
const
|
|
12
|
+
const HttpLogStream_1 = __importDefault(require("../../logging/HttpLogStream"));
|
|
14
13
|
const RemoteLoggerStream_1 = __importDefault(require("../../logging/RemoteLoggerStream"));
|
|
15
14
|
const sentry_1 = require("../../sentry");
|
|
16
15
|
const POLL_INTERVAL_MS = 1_000;
|
|
17
16
|
const UPLOAD_INTERVAL_MS = 5_000;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
'request.finished': 'operation.completed',
|
|
21
|
-
'action.recorded': 'interaction.recorded',
|
|
22
|
-
};
|
|
23
|
-
const AgentDeviceEventSchema = zod_1.z
|
|
24
|
-
.object({
|
|
25
|
-
version: zod_1.z.number(),
|
|
26
|
-
ts: zod_1.z.string(),
|
|
27
|
-
session: zod_1.z.string(),
|
|
28
|
-
kind: zod_1.z.string(),
|
|
29
|
-
requestId: zod_1.z.string().optional().catch(undefined),
|
|
30
|
-
command: zod_1.z.string().optional().catch(undefined),
|
|
31
|
-
status: zod_1.z.string().optional().catch(undefined),
|
|
32
|
-
summary: zod_1.z.string().optional().catch(undefined),
|
|
33
|
-
details: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().catch(undefined),
|
|
34
|
-
})
|
|
35
|
-
.passthrough();
|
|
17
|
+
const EAS_LOGS_THREAD = 'session-events';
|
|
18
|
+
const EAS_LOGS_BUFFER_RETENTION_MS = 30_000;
|
|
36
19
|
const CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
37
20
|
mutation CreateDeviceRunSessionEventLogUploadSession($deviceRunSessionId: ID!) {
|
|
38
21
|
deviceRunSession {
|
|
@@ -45,7 +28,8 @@ const CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION = (0, gql_tada
|
|
|
45
28
|
}
|
|
46
29
|
}
|
|
47
30
|
`);
|
|
48
|
-
async function
|
|
31
|
+
async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
|
|
32
|
+
const { producer } = source;
|
|
49
33
|
let didReportEventLogFailure = false;
|
|
50
34
|
const reportEventLogFailure = (error, operation) => {
|
|
51
35
|
if (didReportEventLogFailure) {
|
|
@@ -54,7 +38,7 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
54
38
|
didReportEventLogFailure = true;
|
|
55
39
|
sentry_1.Sentry.capture('Could not persist device run session events', error, {
|
|
56
40
|
level: 'warning',
|
|
57
|
-
tags: { phase: 'device-run-session-event-collection', operation },
|
|
41
|
+
tags: { phase: 'device-run-session-event-collection', operation, producer },
|
|
58
42
|
extras: { deviceRunSessionId },
|
|
59
43
|
});
|
|
60
44
|
};
|
|
@@ -72,10 +56,31 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
72
56
|
}
|
|
73
57
|
catch (err) {
|
|
74
58
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
75
|
-
logger.warn({ err: error }, 'Could not
|
|
59
|
+
logger.warn({ err: error }, 'Could not persist device run session events to the artifact.');
|
|
76
60
|
reportEventLogFailure(error, 'setup');
|
|
77
61
|
return { stopAsync: async () => { } };
|
|
78
62
|
}
|
|
63
|
+
let didReportRealtimeLogFailure = false;
|
|
64
|
+
const reportRealtimeLogFailure = (error, operation) => {
|
|
65
|
+
if (didReportRealtimeLogFailure) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
didReportRealtimeLogFailure = true;
|
|
69
|
+
sentry_1.Sentry.capture('Could not publish device run session events in real time', error, {
|
|
70
|
+
level: 'warning',
|
|
71
|
+
tags: { phase: 'device-run-session-event-collection', operation, producer },
|
|
72
|
+
extras: { deviceRunSessionId },
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
let realtimeLogStream;
|
|
76
|
+
try {
|
|
77
|
+
realtimeLogStream = createRealtimeLogStream(ctx, logger);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
logger.warn({ err: error }, 'Could not start publishing device run session events in real time.');
|
|
82
|
+
reportRealtimeLogFailure(error, 'setup');
|
|
83
|
+
}
|
|
79
84
|
const states = new Map();
|
|
80
85
|
const controller = new AbortController();
|
|
81
86
|
let parseFailureCount = 0;
|
|
@@ -85,7 +90,7 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
85
90
|
};
|
|
86
91
|
let didReportCollectionFailure = false;
|
|
87
92
|
const collectAsync = async () => {
|
|
88
|
-
const eventFiles = await
|
|
93
|
+
const eventFiles = await source.findEventFilesAsync();
|
|
89
94
|
await Promise.all(eventFiles.map(async (eventFile) => {
|
|
90
95
|
const state = states.get(eventFile) ?? {
|
|
91
96
|
offset: 0,
|
|
@@ -98,18 +103,22 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
98
103
|
await collectEventFileAsync({
|
|
99
104
|
eventFile,
|
|
100
105
|
state,
|
|
106
|
+
source,
|
|
101
107
|
deviceRunSessionId,
|
|
102
|
-
writeEvent: event =>
|
|
108
|
+
writeEvent: event => {
|
|
109
|
+
eventLogStream.write(event);
|
|
110
|
+
realtimeLogStream?.write({ ...event, logId: event.eventId });
|
|
111
|
+
},
|
|
103
112
|
onParseFailure: ({ failure, lineNumber }) => {
|
|
104
113
|
parseFailureCount += 1;
|
|
105
114
|
parseFailureCounts[failure] += 1;
|
|
106
115
|
if (parseFailureCount !== 1) {
|
|
107
116
|
return;
|
|
108
117
|
}
|
|
109
|
-
logger.warn({
|
|
110
|
-
sentry_1.Sentry.capture(
|
|
118
|
+
logger.warn({ producer, eventParseFailure: failure, lineNumber }, `Could not parse an ${producer} event log record.`);
|
|
119
|
+
sentry_1.Sentry.capture(`Could not parse an ${producer} event log record`, {
|
|
111
120
|
level: 'warning',
|
|
112
|
-
tags: { phase:
|
|
121
|
+
tags: { phase: `${producer}-event-collection`, reason: failure },
|
|
113
122
|
extras: { deviceRunSessionId, lineNumber },
|
|
114
123
|
});
|
|
115
124
|
},
|
|
@@ -127,10 +136,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
127
136
|
}
|
|
128
137
|
didReportCollectionFailure = true;
|
|
129
138
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
130
|
-
logger.warn({ err: error },
|
|
131
|
-
sentry_1.Sentry.capture(
|
|
139
|
+
logger.warn({ err: error }, `Could not collect ${producer} events.`);
|
|
140
|
+
sentry_1.Sentry.capture(`Could not collect ${producer} events`, error, {
|
|
132
141
|
level: 'warning',
|
|
133
|
-
tags: { phase:
|
|
142
|
+
tags: { phase: `${producer}-event-collection` },
|
|
134
143
|
extras: { deviceRunSessionId },
|
|
135
144
|
});
|
|
136
145
|
}
|
|
@@ -150,10 +159,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
150
159
|
})()
|
|
151
160
|
.catch(err => {
|
|
152
161
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
153
|
-
logger.warn({ err: error },
|
|
154
|
-
sentry_1.Sentry.capture(
|
|
162
|
+
logger.warn({ err: error }, `Event collection poller for ${producer} failed.`);
|
|
163
|
+
sentry_1.Sentry.capture(`Event collection poller for ${producer} failed`, error, {
|
|
155
164
|
level: 'warning',
|
|
156
|
-
tags: { phase:
|
|
165
|
+
tags: { phase: `${producer}-event-collection`, operation: 'poll' },
|
|
157
166
|
extras: { deviceRunSessionId },
|
|
158
167
|
});
|
|
159
168
|
})
|
|
@@ -166,23 +175,54 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
|
|
|
166
175
|
await pollingPromise;
|
|
167
176
|
await collectSafelyAsync();
|
|
168
177
|
if (parseFailureCount > 1) {
|
|
169
|
-
logger.warn({
|
|
170
|
-
sentry_1.Sentry.capture(
|
|
178
|
+
logger.warn({ producer, eventParseFailures: parseFailureCounts, parseFailureCount }, `Could not parse ${parseFailureCount} ${producer} event log records.`);
|
|
179
|
+
sentry_1.Sentry.capture(`Could not parse multiple ${producer} event log records`, {
|
|
171
180
|
level: 'warning',
|
|
172
|
-
tags: { phase:
|
|
181
|
+
tags: { phase: `${producer}-event-collection` },
|
|
173
182
|
extras: { deviceRunSessionId, parseFailureCount, parseFailureCounts },
|
|
174
183
|
});
|
|
175
184
|
}
|
|
176
|
-
|
|
177
|
-
await eventLogStream.cleanUp();
|
|
178
|
-
}
|
|
179
|
-
catch (err) {
|
|
180
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
181
|
-
logger.warn({ err: error }, 'Could not finish device run session event collection.');
|
|
182
|
-
reportEventLogFailure(error, 'cleanup');
|
|
183
|
-
}
|
|
185
|
+
await Promise.all([cleanUpEventLogStreamAsync(), cleanUpRealtimeLogStreamAsync()]);
|
|
184
186
|
},
|
|
185
187
|
};
|
|
188
|
+
async function cleanUpEventLogStreamAsync() {
|
|
189
|
+
try {
|
|
190
|
+
await eventLogStream.cleanUp();
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
194
|
+
logger.warn({ err: error }, 'Could not finish persisting device run session events.');
|
|
195
|
+
reportEventLogFailure(error, 'cleanup');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function cleanUpRealtimeLogStreamAsync() {
|
|
199
|
+
try {
|
|
200
|
+
await realtimeLogStream?.cleanUp();
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
204
|
+
logger.warn({ err: error }, 'Could not finish publishing device run session events in real time.');
|
|
205
|
+
reportRealtimeLogFailure(error, 'cleanup');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function createRealtimeLogStream(ctx, logger) {
|
|
210
|
+
const baseUrl = ctx.env.EXPO_LOCAL
|
|
211
|
+
? 'http://localhost:4999/logs/'
|
|
212
|
+
: ctx.env.EXPO_STAGING
|
|
213
|
+
? 'https://staging-logs.expo.dev/logs/'
|
|
214
|
+
: undefined;
|
|
215
|
+
const jobRunId = ctx.env.EAS_BUILD_ID;
|
|
216
|
+
const robotAccessToken = ctx.job.secrets?.robotAccessToken;
|
|
217
|
+
if (!baseUrl || !jobRunId || !robotAccessToken) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
return new HttpLogStream_1.default({
|
|
221
|
+
url: new URL(`${jobRunId}/${EAS_LOGS_THREAD}`, baseUrl).toString(),
|
|
222
|
+
headers: { Authorization: `Bearer ${robotAccessToken}` },
|
|
223
|
+
logger,
|
|
224
|
+
bufferRetentionMs: EAS_LOGS_BUFFER_RETENTION_MS,
|
|
225
|
+
});
|
|
186
226
|
}
|
|
187
227
|
async function createEventLogUploadSessionAsync(ctx, deviceRunSessionId) {
|
|
188
228
|
const result = await ctx.graphqlClient
|
|
@@ -199,23 +239,7 @@ async function createEventLogUploadSessionAsync(ctx, deviceRunSessionId) {
|
|
|
199
239
|
headers: uploadSession.headers,
|
|
200
240
|
};
|
|
201
241
|
}
|
|
202
|
-
async function
|
|
203
|
-
const sessionsDir = node_path_1.default.join(stateDir, 'sessions');
|
|
204
|
-
let entries;
|
|
205
|
-
try {
|
|
206
|
-
entries = await node_fs_1.default.promises.readdir(sessionsDir, { withFileTypes: true });
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
if (err.code === 'ENOENT') {
|
|
210
|
-
return [];
|
|
211
|
-
}
|
|
212
|
-
throw err;
|
|
213
|
-
}
|
|
214
|
-
return entries
|
|
215
|
-
.filter(entry => entry.isDirectory())
|
|
216
|
-
.map(entry => node_path_1.default.join(sessionsDir, entry.name, 'events.ndjson'));
|
|
217
|
-
}
|
|
218
|
-
async function collectEventFileAsync({ eventFile, state, deviceRunSessionId, writeEvent, onParseFailure, }) {
|
|
242
|
+
async function collectEventFileAsync({ eventFile, state, source, deviceRunSessionId, writeEvent, onParseFailure, }) {
|
|
219
243
|
let fileSize;
|
|
220
244
|
try {
|
|
221
245
|
fileSize = (await node_fs_1.default.promises.stat(eventFile)).size;
|
|
@@ -234,6 +258,7 @@ async function collectEventFileAsync({ eventFile, state, deviceRunSessionId, wri
|
|
|
234
258
|
if (fileSize === state.offset) {
|
|
235
259
|
return;
|
|
236
260
|
}
|
|
261
|
+
const sourceKey = source.sourceKeyForFile(eventFile);
|
|
237
262
|
const handle = await node_fs_1.default.promises.open(eventFile, 'r');
|
|
238
263
|
try {
|
|
239
264
|
const buffer = new Uint8Array(new ArrayBuffer(fileSize - state.offset));
|
|
@@ -245,67 +270,22 @@ async function collectEventFileAsync({ eventFile, state, deviceRunSessionId, wri
|
|
|
245
270
|
for (const line of lines) {
|
|
246
271
|
const lineNumber = state.nextLineNumber++;
|
|
247
272
|
const sequenceNumber = state.nextSequenceNumber++;
|
|
248
|
-
const { event, failure } =
|
|
273
|
+
const { event, failure } = source.parseLine({
|
|
274
|
+
line,
|
|
275
|
+
sourceKey,
|
|
276
|
+
sequenceNumber,
|
|
277
|
+
deviceRunSessionId,
|
|
278
|
+
});
|
|
249
279
|
if (failure) {
|
|
250
280
|
onParseFailure({ failure, lineNumber });
|
|
251
281
|
}
|
|
252
282
|
if (!event) {
|
|
253
283
|
continue;
|
|
254
284
|
}
|
|
255
|
-
|
|
256
|
-
event,
|
|
257
|
-
sequenceNumber,
|
|
258
|
-
sourceSessionDirectory: node_path_1.default.basename(node_path_1.default.dirname(eventFile)),
|
|
259
|
-
deviceRunSessionId,
|
|
260
|
-
});
|
|
261
|
-
writeEvent(deviceRunSessionEvent);
|
|
285
|
+
writeEvent(event);
|
|
262
286
|
}
|
|
263
287
|
}
|
|
264
288
|
finally {
|
|
265
289
|
await handle.close();
|
|
266
290
|
}
|
|
267
291
|
}
|
|
268
|
-
function parseAgentDeviceEvent(line) {
|
|
269
|
-
if (!line.trim()) {
|
|
270
|
-
return {};
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
const result = AgentDeviceEventSchema.safeParse(JSON.parse(line));
|
|
274
|
-
return result.success ? { event: result.data } : { failure: 'invalid-event' };
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return { failure: 'invalid-json' };
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function normalizeAgentDeviceEvent({ event, sequenceNumber, sourceSessionDirectory, deviceRunSessionId, }) {
|
|
281
|
-
const type = AGENT_DEVICE_EVENT_KIND_TO_TYPE[event.kind] ?? event.kind;
|
|
282
|
-
const durationMs = event.details?.durationMs;
|
|
283
|
-
const outcome = event.status === 'ok' ? 'success' : event.status === 'error' ? 'failure' : undefined;
|
|
284
|
-
const summary = event.summary ??
|
|
285
|
-
(event.kind === 'request.started'
|
|
286
|
-
? `Started ${event.command ?? 'activity'}`
|
|
287
|
-
: event.kind === 'request.finished'
|
|
288
|
-
? `Finished ${event.command ?? 'activity'}`
|
|
289
|
-
: event.kind === 'action.recorded'
|
|
290
|
-
? `Recorded ${event.command ?? 'activity'}`
|
|
291
|
-
: `${event.kind}${event.command ? `: ${event.command}` : ''}`);
|
|
292
|
-
return {
|
|
293
|
-
v: 1,
|
|
294
|
-
// Consumers use eventId to deduplicate events across polls. Keep the per-file sequence
|
|
295
|
-
// monotonic across source-file truncations so an ID is never reused during collection.
|
|
296
|
-
eventId: `agent-device:${deviceRunSessionId}:${sourceSessionDirectory}:${sequenceNumber}`,
|
|
297
|
-
ts: event.ts,
|
|
298
|
-
producer: 'agent-device',
|
|
299
|
-
type,
|
|
300
|
-
...(event.requestId ? { operationId: event.requestId } : {}),
|
|
301
|
-
...(outcome ? { outcome } : {}),
|
|
302
|
-
...(typeof durationMs === 'number' ? { durationMs } : {}),
|
|
303
|
-
summary,
|
|
304
|
-
data: {
|
|
305
|
-
...event.details,
|
|
306
|
-
session: event.session,
|
|
307
|
-
sourceVersion: event.version,
|
|
308
|
-
...(event.status ? { sourceStatus: event.status } : {}),
|
|
309
|
-
},
|
|
310
|
-
};
|
|
311
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
|
-
import { BuildStepEnv } from '@expo/steps';
|
|
2
|
+
import { BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { CustomBuildContext } from '../../customBuildContext';
|
|
5
5
|
export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
|
|
@@ -21,6 +21,26 @@ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSess
|
|
|
21
21
|
logger: bunyan;
|
|
22
22
|
signal?: AbortSignal;
|
|
23
23
|
}): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Install ffmpeg when the runtime does not already provide it, so argent's
|
|
26
|
+
* `screen-recording-start` tool can encode a video. The worker images do not
|
|
27
|
+
* ship ffmpeg yet, so without this the tool fails with "`ffmpeg` was not found
|
|
28
|
+
* on PATH" — on macOS (iOS simulators) and Linux (Android emulators) alike.
|
|
29
|
+
*
|
|
30
|
+
* Best-effort by design: screen recording is one optional argent tool, so a
|
|
31
|
+
* failure here is logged and the session continues without it.
|
|
32
|
+
*
|
|
33
|
+
* The whole body is wrapped because the caller runs this in the background with
|
|
34
|
+
* `void`. There is no unhandledRejection handler in the worker, so a rejection
|
|
35
|
+
* escaping here would crash the process and take the live session with it.
|
|
36
|
+
* `spawn` is not an async function and can throw synchronously, which
|
|
37
|
+
* `asyncResult` cannot catch — it only wraps an already-created promise.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureFfmpegInstalledAsync({ runtimePlatform, env, logger, }: {
|
|
40
|
+
runtimePlatform: BuildRuntimePlatform;
|
|
41
|
+
env: BuildStepEnv;
|
|
42
|
+
logger: bunyan;
|
|
43
|
+
}): Promise<void>;
|
|
24
44
|
/**
|
|
25
45
|
* Translate Cloudflare ICE servers into serve-sim CLI flags: `--stun-url` (the
|
|
26
46
|
* credential-less entries) and `--turn-url`/`--turn-username`/`--turn-credential`
|
|
@@ -51,6 +71,7 @@ export type DetachedProcessHandle = {
|
|
|
51
71
|
/** PID of the directly spawned process, if the OS assigned one. */
|
|
52
72
|
pid: number | undefined;
|
|
53
73
|
getOutput: () => string;
|
|
74
|
+
stopAsync: () => Promise<void>;
|
|
54
75
|
};
|
|
55
76
|
export declare function spawnDetached({ command, args, cwd, env, }: {
|
|
56
77
|
command: string;
|
|
@@ -58,14 +79,31 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
|
|
|
58
79
|
cwd?: string;
|
|
59
80
|
env: BuildStepEnv;
|
|
60
81
|
}): DetachedProcessHandle;
|
|
82
|
+
export declare function metricsCorsOriginToServeSimArgs(env: BuildStepEnv): string[];
|
|
83
|
+
export declare function createServeSimArgs({ port, turnArgs, metricsCorsArgs, }: {
|
|
84
|
+
port: number;
|
|
85
|
+
turnArgs?: string[];
|
|
86
|
+
metricsCorsArgs?: string[];
|
|
87
|
+
}): string[];
|
|
88
|
+
export declare function waitForServeSimReadyAsync({ serveSim, port, timeoutMs, }: {
|
|
89
|
+
serveSim: Pick<DetachedProcessHandle, 'pid' | 'getOutput'>;
|
|
90
|
+
port: number;
|
|
91
|
+
timeoutMs: number;
|
|
92
|
+
}): Promise<void>;
|
|
93
|
+
export type ServeSimPreviewHandle = {
|
|
94
|
+
previewUrl: string;
|
|
95
|
+
stopAsync: () => Promise<void>;
|
|
96
|
+
};
|
|
61
97
|
export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, }: {
|
|
62
98
|
baseDomain: string;
|
|
63
99
|
env: BuildStepEnv;
|
|
64
100
|
logger: bunyan;
|
|
65
101
|
timeoutMs: number;
|
|
66
|
-
}): Promise<
|
|
67
|
-
|
|
68
|
-
|
|
102
|
+
}): Promise<ServeSimPreviewHandle>;
|
|
103
|
+
export type NgrokTunnelHandle = {
|
|
104
|
+
url: string;
|
|
105
|
+
stopAsync: () => Promise<void>;
|
|
106
|
+
};
|
|
69
107
|
export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, rewriteHostHeader, logger, }: {
|
|
70
108
|
port: number;
|
|
71
109
|
subdomainPrefix: string;
|
|
@@ -73,7 +111,7 @@ export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomai
|
|
|
73
111
|
authtoken: string;
|
|
74
112
|
rewriteHostHeader?: boolean;
|
|
75
113
|
logger: bunyan;
|
|
76
|
-
}): Promise<
|
|
114
|
+
}): Promise<NgrokTunnelHandle>;
|
|
77
115
|
export declare function waitForFileAsync<T>({ filePath, timeoutMs, description, parse, }: {
|
|
78
116
|
filePath: string;
|
|
79
117
|
timeoutMs: number;
|