@expo/build-tools 21.0.3 → 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.
@@ -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 DAEMON_JSON_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device', 'daemon.json');
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, remoteDeviceRunSession_1.waitForDeviceRunSessionStoppedAsync)({
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
+ }>;
@@ -0,0 +1,311 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startAgentDeviceEventCollectionAsync = startAgentDeviceEventCollectionAsync;
7
+ const eas_build_job_1 = require("@expo/eas-build-job");
8
+ const gql_tada_1 = require("gql.tada");
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const node_string_decoder_1 = require("node:string_decoder");
12
+ const promises_1 = require("node:timers/promises");
13
+ const zod_1 = require("zod");
14
+ const RemoteLoggerStream_1 = __importDefault(require("../../logging/RemoteLoggerStream"));
15
+ const sentry_1 = require("../../sentry");
16
+ const POLL_INTERVAL_MS = 1_000;
17
+ const UPLOAD_INTERVAL_MS = 5_000;
18
+ const AGENT_DEVICE_EVENT_KIND_TO_TYPE = {
19
+ 'request.started': 'operation.started',
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();
36
+ const CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
37
+ mutation CreateDeviceRunSessionEventLogUploadSession($deviceRunSessionId: ID!) {
38
+ deviceRunSession {
39
+ createEventLogUploadSession(deviceRunSessionId: $deviceRunSessionId) {
40
+ uploadSession {
41
+ url
42
+ headers
43
+ }
44
+ }
45
+ }
46
+ }
47
+ `);
48
+ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
49
+ let didReportEventLogFailure = false;
50
+ const reportEventLogFailure = (error, operation) => {
51
+ if (didReportEventLogFailure) {
52
+ return;
53
+ }
54
+ didReportEventLogFailure = true;
55
+ sentry_1.Sentry.capture('Could not persist device run session events', error, {
56
+ level: 'warning',
57
+ tags: { phase: 'device-run-session-event-collection', operation },
58
+ extras: { deviceRunSessionId },
59
+ });
60
+ };
61
+ let eventLogStream;
62
+ try {
63
+ const uploadSession = await createEventLogUploadSessionAsync(ctx, deviceRunSessionId);
64
+ eventLogStream = new RemoteLoggerStream_1.default({
65
+ logger,
66
+ uploadMethod: { signedUrl: uploadSession },
67
+ options: {
68
+ uploadIntervalMs: UPLOAD_INTERVAL_MS,
69
+ },
70
+ });
71
+ await eventLogStream.init();
72
+ }
73
+ catch (err) {
74
+ const error = err instanceof Error ? err : new Error(String(err));
75
+ logger.warn({ err: error }, 'Could not start device run session event collection.');
76
+ reportEventLogFailure(error, 'setup');
77
+ return { stopAsync: async () => { } };
78
+ }
79
+ const states = new Map();
80
+ const controller = new AbortController();
81
+ let parseFailureCount = 0;
82
+ const parseFailureCounts = {
83
+ 'invalid-json': 0,
84
+ 'invalid-event': 0,
85
+ };
86
+ let didReportCollectionFailure = false;
87
+ const collectAsync = async () => {
88
+ const eventFiles = await findAgentDeviceEventFilesAsync(stateDir);
89
+ await Promise.all(eventFiles.map(async (eventFile) => {
90
+ const state = states.get(eventFile) ?? {
91
+ offset: 0,
92
+ nextSequenceNumber: 1,
93
+ nextLineNumber: 1,
94
+ pending: '',
95
+ decoder: new node_string_decoder_1.StringDecoder('utf8'),
96
+ };
97
+ states.set(eventFile, state);
98
+ await collectEventFileAsync({
99
+ eventFile,
100
+ state,
101
+ deviceRunSessionId,
102
+ writeEvent: event => eventLogStream.write(event),
103
+ onParseFailure: ({ failure, lineNumber }) => {
104
+ parseFailureCount += 1;
105
+ parseFailureCounts[failure] += 1;
106
+ if (parseFailureCount !== 1) {
107
+ return;
108
+ }
109
+ logger.warn({ agentDeviceEventParseFailure: failure, lineNumber }, 'Could not parse an agent-device event log record.');
110
+ sentry_1.Sentry.capture('Could not parse an agent-device event log record', {
111
+ level: 'warning',
112
+ tags: { phase: 'agent-device-event-collection', reason: failure },
113
+ extras: { deviceRunSessionId, lineNumber },
114
+ });
115
+ },
116
+ });
117
+ }));
118
+ };
119
+ const collectSafelyAsync = async () => {
120
+ try {
121
+ await collectAsync();
122
+ didReportCollectionFailure = false;
123
+ }
124
+ catch (err) {
125
+ if (didReportCollectionFailure) {
126
+ return;
127
+ }
128
+ didReportCollectionFailure = true;
129
+ const error = err instanceof Error ? err : new Error(String(err));
130
+ logger.warn({ err: error }, 'Could not collect agent-device events.');
131
+ sentry_1.Sentry.capture('Could not collect agent-device events', error, {
132
+ level: 'warning',
133
+ tags: { phase: 'agent-device-event-collection' },
134
+ extras: { deviceRunSessionId },
135
+ });
136
+ }
137
+ };
138
+ const pollingPromise = (async () => {
139
+ while (!controller.signal.aborted) {
140
+ await collectSafelyAsync();
141
+ try {
142
+ await (0, promises_1.setTimeout)(pollIntervalMs, undefined, { signal: controller.signal });
143
+ }
144
+ catch (err) {
145
+ if (!controller.signal.aborted) {
146
+ throw err;
147
+ }
148
+ }
149
+ }
150
+ })()
151
+ .catch(err => {
152
+ const error = err instanceof Error ? err : new Error(String(err));
153
+ logger.warn({ err: error }, 'Agent-device event collection poller failed.');
154
+ sentry_1.Sentry.capture('Agent-device event collection poller failed', error, {
155
+ level: 'warning',
156
+ tags: { phase: 'agent-device-event-collection', operation: 'poll' },
157
+ extras: { deviceRunSessionId },
158
+ });
159
+ })
160
+ .catch(() => {
161
+ // A diagnostics failure must not recreate an unhandled rejection in this best-effort poller.
162
+ });
163
+ return {
164
+ stopAsync: async () => {
165
+ controller.abort();
166
+ await pollingPromise;
167
+ await collectSafelyAsync();
168
+ if (parseFailureCount > 1) {
169
+ logger.warn({ agentDeviceEventParseFailures: parseFailureCounts, parseFailureCount }, `Could not parse ${parseFailureCount} agent-device event log records.`);
170
+ sentry_1.Sentry.capture('Could not parse multiple agent-device event log records', {
171
+ level: 'warning',
172
+ tags: { phase: 'agent-device-event-collection' },
173
+ extras: { deviceRunSessionId, parseFailureCount, parseFailureCounts },
174
+ });
175
+ }
176
+ try {
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
+ }
184
+ },
185
+ };
186
+ }
187
+ async function createEventLogUploadSessionAsync(ctx, deviceRunSessionId) {
188
+ const result = await ctx.graphqlClient
189
+ .mutation(CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION, {
190
+ deviceRunSessionId,
191
+ })
192
+ .toPromise();
193
+ if (result.error) {
194
+ throw new eas_build_job_1.SystemError(`Failed to create device run session event log upload session: ${result.error.message}`, { cause: result.error });
195
+ }
196
+ const uploadSession = result.data.deviceRunSession.createEventLogUploadSession.uploadSession;
197
+ return {
198
+ url: uploadSession.url,
199
+ headers: uploadSession.headers,
200
+ };
201
+ }
202
+ async function findAgentDeviceEventFilesAsync(stateDir) {
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, }) {
219
+ let fileSize;
220
+ try {
221
+ fileSize = (await node_fs_1.default.promises.stat(eventFile)).size;
222
+ }
223
+ catch (err) {
224
+ if (err.code === 'ENOENT') {
225
+ return;
226
+ }
227
+ throw err;
228
+ }
229
+ if (fileSize < state.offset) {
230
+ state.offset = 0;
231
+ state.pending = '';
232
+ state.decoder = new node_string_decoder_1.StringDecoder('utf8');
233
+ }
234
+ if (fileSize === state.offset) {
235
+ return;
236
+ }
237
+ const handle = await node_fs_1.default.promises.open(eventFile, 'r');
238
+ try {
239
+ const buffer = new Uint8Array(new ArrayBuffer(fileSize - state.offset));
240
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, state.offset);
241
+ state.offset += bytesRead;
242
+ const text = state.decoder.write(buffer.subarray(0, bytesRead));
243
+ const lines = `${state.pending}${text}`.split('\n');
244
+ state.pending = lines.pop() ?? '';
245
+ for (const line of lines) {
246
+ const lineNumber = state.nextLineNumber++;
247
+ const sequenceNumber = state.nextSequenceNumber++;
248
+ const { event, failure } = parseAgentDeviceEvent(line);
249
+ if (failure) {
250
+ onParseFailure({ failure, lineNumber });
251
+ }
252
+ if (!event) {
253
+ continue;
254
+ }
255
+ const deviceRunSessionEvent = normalizeAgentDeviceEvent({
256
+ event,
257
+ sequenceNumber,
258
+ sourceSessionDirectory: node_path_1.default.basename(node_path_1.default.dirname(eventFile)),
259
+ deviceRunSessionId,
260
+ });
261
+ writeEvent(deviceRunSessionEvent);
262
+ }
263
+ }
264
+ finally {
265
+ await handle.close();
266
+ }
267
+ }
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.0.3",
3
+ "version": "21.1.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -39,14 +39,14 @@
39
39
  "@expo/config": "55.0.10",
40
40
  "@expo/config-plugins": "55.0.7",
41
41
  "@expo/downloader": "21.0.0",
42
- "@expo/eas-build-job": "21.0.3",
42
+ "@expo/eas-build-job": "21.1.0",
43
43
  "@expo/env": "^0.4.0",
44
44
  "@expo/logger": "21.0.0",
45
45
  "@expo/package-manager": "1.9.10",
46
46
  "@expo/plist": "^0.3.5",
47
47
  "@expo/results": "^1.0.0",
48
48
  "@expo/spawn-async": "1.7.2",
49
- "@expo/steps": "21.0.3",
49
+ "@expo/steps": "21.1.0",
50
50
  "@expo/template-file": "21.0.2",
51
51
  "@expo/turtle-spawn": "21.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "62d46fece55ab5b27c2a572fc91158214b9ae96b"
103
+ "gitHead": "24272d8a5f080584f9688d19c2a4a4d06f9448ac"
104
104
  }