@expo/build-tools 21.3.0 → 21.4.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.
@@ -13,7 +13,7 @@ const node_os_1 = __importDefault(require("node:os"));
13
13
  const node_path_1 = __importDefault(require("node:path"));
14
14
  const sentry_1 = require("../../sentry");
15
15
  const agentDeviceArtifacts_1 = require("../utils/agentDeviceArtifacts");
16
- const deviceRunSessionEvents_1 = require("../utils/deviceRunSessionEvents");
16
+ const agentDeviceEvents_1 = require("../utils/agentDeviceEvents");
17
17
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
18
18
  const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
19
19
  const AGENT_DEVICE_REPO_URL = 'https://github.com/callstack/agent-device.git';
@@ -96,7 +96,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
96
96
  daemonToken,
97
97
  logger,
98
98
  });
99
- const eventCollection = await (0, deviceRunSessionEvents_1.startAgentDeviceEventCollectionAsync)({
99
+ const eventCollection = await (0, agentDeviceEvents_1.startAgentDeviceEventCollectionAsync)({
100
100
  ctx,
101
101
  deviceRunSessionId,
102
102
  stateDir: AGENT_DEVICE_STATE_DIR,
@@ -2,7 +2,7 @@ import { type bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
3
  import { z } from 'zod';
4
4
  import { CustomBuildContext } from '../../customBuildContext';
5
- export declare const MIN_ARGENT_REMOTE_SESSION_VERSION = "0.12.0";
5
+ export declare const MIN_ARGENT_REMOTE_SESSION_VERSION = "0.16.0";
6
6
  declare const ArgentToolServerStateSchema: z.ZodObject<{
7
7
  port: z.ZodNumber;
8
8
  pid: z.ZodNumber;
@@ -10,6 +10,13 @@ declare const ArgentToolServerStateSchema: z.ZodObject<{
10
10
  }, z.core.$strip>;
11
11
  type ArgentToolServerState = z.infer<typeof ArgentToolServerStateSchema>;
12
12
  export declare function createStartArgentRemoteSessionBuildFunction(ctx: CustomBuildContext): BuildFunction;
13
+ export declare function stopArgentEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }: {
14
+ eventCollection: {
15
+ stopAsync: () => Promise<void>;
16
+ };
17
+ deviceRunSessionId: string;
18
+ logger: bunyan;
19
+ }): Promise<void>;
13
20
  export declare function warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger, }: {
14
21
  packageVersion: string | undefined;
15
22
  logger: bunyan;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.MIN_ARGENT_REMOTE_SESSION_VERSION = void 0;
7
7
  exports.createStartArgentRemoteSessionBuildFunction = createStartArgentRemoteSessionBuildFunction;
8
+ exports.stopArgentEventCollectionSafelyAsync = stopArgentEventCollectionSafelyAsync;
8
9
  exports.warnIfArgentPackageVersionCannotBeVerified = warnIfArgentPackageVersionCannotBeVerified;
9
10
  exports.waitForArgentToolServerStateAsync = waitForArgentToolServerStateAsync;
10
11
  const eas_build_job_1 = require("@expo/eas-build-job");
@@ -19,11 +20,20 @@ const sentry_1 = require("../../sentry");
19
20
  const processes_1 = require("../../utils/processes");
20
21
  const retry_1 = require("../../utils/retry");
21
22
  const argentArtifacts_1 = require("../utils/argentArtifacts");
23
+ const argentEvents_1 = require("../utils/argentEvents");
22
24
  const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
23
25
  const ARGENT_PACKAGE_NAME = '@swmansion/argent';
24
- exports.MIN_ARGENT_REMOTE_SESSION_VERSION = '0.12.0';
26
+ // 0.16.0 is the first version that exposes the tool-server event log flag; keeping the floor
27
+ // here lets us enable it (and the artifacts list endpoint) unconditionally below.
28
+ exports.MIN_ARGENT_REMOTE_SESSION_VERSION = '0.16.0';
25
29
  const ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG = 'artifacts-list-endpoint';
30
+ // Tells the tool-server to write its structured event log so we can collect session events.
31
+ const ARGENT_EVENT_LOG_FLAG = 'tool-server-event-log';
26
32
  const ARGENT_STATE_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.argent');
33
+ // Pin the event log path explicitly and hand the same value to the tool-server (via
34
+ // ARGENT_EVENT_LOG) and the collector, so an ambient ARGENT_EVENT_LOG or a future change to
35
+ // Argent's default can never make the two disagree.
36
+ const ARGENT_EVENT_LOG_PATH = node_path_1.default.join(ARGENT_STATE_DIR, argentEvents_1.ARGENT_EVENT_LOG_FILENAME);
27
37
  const STARTUP_TIMEOUT_MS = 60_000;
28
38
  const ArgentToolServerStateSchema = zod_1.z.object({
29
39
  port: zod_1.z.number(),
@@ -61,6 +71,8 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
61
71
  }
62
72
  logger.info('Enabling the Argent artifacts list endpoint flag.');
63
73
  await (0, turtle_spawn_1.default)('bunx', [`${ARGENT_PACKAGE_NAME}@${versionSpec}`, 'enable', ARGENT_ARTIFACTS_LIST_ENDPOINT_FLAG], { env, logger });
74
+ logger.info('Enabling the Argent tool-server event log flag.');
75
+ await (0, turtle_spawn_1.default)('bunx', [`${ARGENT_PACKAGE_NAME}@${versionSpec}`, 'enable', ARGENT_EVENT_LOG_FLAG], { env, logger });
64
76
  logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} tool-server via bunx.`);
65
77
  // Keep Argent itself in foreground mode under the detached bunx process. This preserves
66
78
  // the bunx -> Argent CLI -> tool-server ancestry used to identify the matching state file.
@@ -76,7 +88,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
76
88
  '0',
77
89
  '--force',
78
90
  ],
79
- env,
91
+ env: { ...env, ARGENT_EVENT_LOG: ARGENT_EVENT_LOG_PATH },
80
92
  });
81
93
  if (argentServer.pid === undefined) {
82
94
  throw new eas_build_job_1.SystemError('Failed to start Argent: could not determine the PID of the launched process.');
@@ -109,6 +121,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
109
121
  logger,
110
122
  signal: artifactPollSignal,
111
123
  });
124
+ const eventCollection = await (0, argentEvents_1.startArgentEventCollectionAsync)({
125
+ ctx,
126
+ deviceRunSessionId,
127
+ eventLogPath: ARGENT_EVENT_LOG_PATH,
128
+ logger,
129
+ });
112
130
  try {
113
131
  const publicToolsUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
114
132
  port: toolServerPort,
@@ -149,6 +167,7 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
149
167
  });
150
168
  }
151
169
  finally {
170
+ await stopArgentEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger });
152
171
  artifactPollAbortController.abort();
153
172
  try {
154
173
  await artifactPollingPromise;
@@ -162,6 +181,20 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
162
181
  },
163
182
  });
164
183
  }
184
+ async function stopArgentEventCollectionSafelyAsync({ eventCollection, deviceRunSessionId, logger, }) {
185
+ try {
186
+ await eventCollection.stopAsync();
187
+ }
188
+ catch (err) {
189
+ const error = err instanceof Error ? err : new Error(String(err));
190
+ sentry_1.Sentry.capture('Could not finish argent session event collection', error, {
191
+ level: 'warning',
192
+ tags: { phase: 'argent-event-collection', operation: 'stop' },
193
+ extras: { deviceRunSessionId },
194
+ });
195
+ logger.warn({ err: error }, 'Could not finish argent session event collection.');
196
+ }
197
+ }
165
198
  function warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger, }) {
166
199
  if (!packageVersion || packageVersion === 'latest') {
167
200
  return;
@@ -0,0 +1,11 @@
1
+ import { type bunyan } from '@expo/logger';
2
+ import { type CustomBuildContext } from '../../customBuildContext';
3
+ export declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
4
+ ctx: CustomBuildContext;
5
+ deviceRunSessionId: string;
6
+ stateDir: string;
7
+ logger: bunyan;
8
+ pollIntervalMs?: number;
9
+ }): Promise<{
10
+ stopAsync: () => Promise<void>;
11
+ }>;
@@ -0,0 +1,116 @@
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 node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const zod_1 = require("zod");
10
+ const deviceRunSessionEvents_1 = require("./deviceRunSessionEvents");
11
+ const AGENT_DEVICE_PRODUCER = 'agent-device';
12
+ const AGENT_DEVICE_EVENT_KIND_TO_TYPE = {
13
+ 'request.started': 'operation.started',
14
+ 'request.finished': 'operation.completed',
15
+ 'action.recorded': 'interaction.recorded',
16
+ };
17
+ const AgentDeviceEventSchema = zod_1.z
18
+ .object({
19
+ version: zod_1.z.number(),
20
+ ts: zod_1.z.string(),
21
+ session: zod_1.z.string(),
22
+ kind: zod_1.z.string(),
23
+ requestId: zod_1.z.string().optional().catch(undefined),
24
+ command: zod_1.z.string().optional().catch(undefined),
25
+ status: zod_1.z.string().optional().catch(undefined),
26
+ summary: zod_1.z.string().optional().catch(undefined),
27
+ details: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional().catch(undefined),
28
+ })
29
+ .passthrough();
30
+ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }) {
31
+ return (0, deviceRunSessionEvents_1.startDeviceRunSessionEventCollectionAsync)({
32
+ ctx,
33
+ deviceRunSessionId,
34
+ logger,
35
+ pollIntervalMs,
36
+ source: {
37
+ producer: AGENT_DEVICE_PRODUCER,
38
+ findEventFilesAsync: () => findAgentDeviceEventFilesAsync(stateDir),
39
+ sourceKeyForFile: eventFile => node_path_1.default.basename(node_path_1.default.dirname(eventFile)),
40
+ parseLine: ({ line, sourceKey, sequenceNumber, deviceRunSessionId }) => {
41
+ const { event, failure } = parseAgentDeviceEvent(line);
42
+ if (!event) {
43
+ return { failure };
44
+ }
45
+ return {
46
+ event: normalizeAgentDeviceEvent({
47
+ event,
48
+ sequenceNumber,
49
+ sourceSessionDirectory: sourceKey,
50
+ deviceRunSessionId,
51
+ }),
52
+ };
53
+ },
54
+ },
55
+ });
56
+ }
57
+ async function findAgentDeviceEventFilesAsync(stateDir) {
58
+ const sessionsDir = node_path_1.default.join(stateDir, 'sessions');
59
+ let entries;
60
+ try {
61
+ entries = await node_fs_1.default.promises.readdir(sessionsDir, { withFileTypes: true });
62
+ }
63
+ catch (err) {
64
+ if (err.code === 'ENOENT') {
65
+ return [];
66
+ }
67
+ throw err;
68
+ }
69
+ return entries
70
+ .filter(entry => entry.isDirectory())
71
+ .map(entry => node_path_1.default.join(sessionsDir, entry.name, 'events.ndjson'));
72
+ }
73
+ function parseAgentDeviceEvent(line) {
74
+ if (!line.trim()) {
75
+ return {};
76
+ }
77
+ try {
78
+ const result = AgentDeviceEventSchema.safeParse(JSON.parse(line));
79
+ return result.success ? { event: result.data } : { failure: 'invalid-event' };
80
+ }
81
+ catch {
82
+ return { failure: 'invalid-json' };
83
+ }
84
+ }
85
+ function normalizeAgentDeviceEvent({ event, sequenceNumber, sourceSessionDirectory, deviceRunSessionId, }) {
86
+ const type = AGENT_DEVICE_EVENT_KIND_TO_TYPE[event.kind] ?? event.kind;
87
+ const durationMs = event.details?.durationMs;
88
+ const outcome = event.status === 'ok' ? 'success' : event.status === 'error' ? 'failure' : undefined;
89
+ const summary = event.summary ??
90
+ (event.kind === 'request.started'
91
+ ? `Started ${event.command ?? 'activity'}`
92
+ : event.kind === 'request.finished'
93
+ ? `Finished ${event.command ?? 'activity'}`
94
+ : event.kind === 'action.recorded'
95
+ ? `Recorded ${event.command ?? 'activity'}`
96
+ : `${event.kind}${event.command ? `: ${event.command}` : ''}`);
97
+ return {
98
+ v: 1,
99
+ // Consumers use eventId to deduplicate events across polls. Keep the per-file sequence
100
+ // monotonic across source-file truncations so an ID is never reused during collection.
101
+ eventId: `agent-device:${deviceRunSessionId}:${sourceSessionDirectory}:${sequenceNumber}`,
102
+ ts: event.ts,
103
+ producer: 'agent-device',
104
+ type,
105
+ ...(event.requestId ? { operationId: event.requestId } : {}),
106
+ ...(outcome ? { outcome } : {}),
107
+ ...(typeof durationMs === 'number' ? { durationMs } : {}),
108
+ summary,
109
+ data: {
110
+ ...event.details,
111
+ session: event.session,
112
+ sourceVersion: event.version,
113
+ ...(event.status ? { sourceStatus: event.status } : {}),
114
+ },
115
+ };
116
+ }
@@ -0,0 +1,12 @@
1
+ import { type bunyan } from '@expo/logger';
2
+ import { type CustomBuildContext } from '../../customBuildContext';
3
+ export declare const ARGENT_EVENT_LOG_FILENAME = "tool-server-events.jsonl";
4
+ export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionId, eventLogPath, logger, pollIntervalMs, }: {
5
+ ctx: CustomBuildContext;
6
+ deviceRunSessionId: string;
7
+ eventLogPath: string;
8
+ logger: bunyan;
9
+ pollIntervalMs?: number;
10
+ }): Promise<{
11
+ stopAsync: () => Promise<void>;
12
+ }>;
@@ -0,0 +1,131 @@
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.ARGENT_EVENT_LOG_FILENAME = void 0;
7
+ exports.startArgentEventCollectionAsync = startArgentEventCollectionAsync;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const zod_1 = require("zod");
11
+ const deviceRunSessionEvents_1 = require("./deviceRunSessionEvents");
12
+ const ARGENT_PRODUCER = 'argent';
13
+ // Argent's tool-server writes its event log to `~/.argent/tool-server-events.jsonl`
14
+ // (its default $ARGENT_EVENT_LOG path) once the `tool-server-event-log` flag is on.
15
+ exports.ARGENT_EVENT_LOG_FILENAME = 'tool-server-events.jsonl';
16
+ // Argent records are bunyan log lines tagged with a `type`. Map the tool-lifecycle
17
+ // types onto the shared operation vocabulary; anything else (service.*,
18
+ // tool_server.*, future types) passes through unchanged, mirroring agent-device.
19
+ const ARGENT_EVENT_TYPE_TO_TYPE = {
20
+ 'tool.invoked': 'operation.started',
21
+ 'tool.completed': 'operation.completed',
22
+ 'tool.failed': 'operation.completed',
23
+ };
24
+ // Bunyan bookkeeping plus the fields we promote to the top level of a
25
+ // DeviceRunSessionEvent — excluded from the free-form `data` bag so it carries
26
+ // only the record's own context (toolId, serviceId, failureSignal, ...).
27
+ const NON_DATA_KEYS = new Set([
28
+ 'name',
29
+ 'hostname',
30
+ 'pid',
31
+ 'v',
32
+ 'time',
33
+ 'msg',
34
+ 'level',
35
+ 'type',
36
+ 'toolInvocationId',
37
+ 'durationMs',
38
+ ]);
39
+ const ArgentEventSchema = zod_1.z
40
+ .object({
41
+ time: zod_1.z.string(),
42
+ type: zod_1.z.string(),
43
+ // Argent's EventLogRecord requires `msg` and every emit site sets it to the
44
+ // human-readable description of the event, so it is our summary verbatim.
45
+ msg: zod_1.z.string(),
46
+ level: zod_1.z.number().optional().catch(undefined),
47
+ toolId: zod_1.z.string().optional().catch(undefined),
48
+ toolInvocationId: zod_1.z.string().optional().catch(undefined),
49
+ durationMs: zod_1.z.number().optional().catch(undefined),
50
+ })
51
+ .passthrough();
52
+ async function startArgentEventCollectionAsync({ ctx, deviceRunSessionId, eventLogPath, logger, pollIntervalMs, }) {
53
+ return (0, deviceRunSessionEvents_1.startDeviceRunSessionEventCollectionAsync)({
54
+ ctx,
55
+ deviceRunSessionId,
56
+ logger,
57
+ pollIntervalMs,
58
+ source: {
59
+ producer: ARGENT_PRODUCER,
60
+ findEventFilesAsync: () => findArgentEventFilesAsync(eventLogPath),
61
+ sourceKeyForFile: eventFile => node_path_1.default.basename(eventFile, node_path_1.default.extname(eventFile)),
62
+ parseLine: ({ line, sourceKey, sequenceNumber, deviceRunSessionId }) => {
63
+ const { event, failure } = parseArgentEvent(line);
64
+ if (!event) {
65
+ return { failure };
66
+ }
67
+ return {
68
+ event: normalizeArgentEvent({ event, sequenceNumber, sourceKey, deviceRunSessionId }),
69
+ };
70
+ },
71
+ },
72
+ });
73
+ }
74
+ async function findArgentEventFilesAsync(eventLogPath) {
75
+ try {
76
+ await node_fs_1.default.promises.access(eventLogPath);
77
+ return [eventLogPath];
78
+ }
79
+ catch (err) {
80
+ // The tool-server has not created the file yet — expected until the first event is written.
81
+ // Surface any other error (permissions, I/O) to the engine so it reaches Sentry.
82
+ if (err.code === 'ENOENT') {
83
+ return [];
84
+ }
85
+ throw err;
86
+ }
87
+ }
88
+ function parseArgentEvent(line) {
89
+ if (!line.trim()) {
90
+ return {};
91
+ }
92
+ try {
93
+ const result = ArgentEventSchema.safeParse(JSON.parse(line));
94
+ return result.success ? { event: result.data } : { failure: 'invalid-event' };
95
+ }
96
+ catch {
97
+ return { failure: 'invalid-json' };
98
+ }
99
+ }
100
+ function normalizeArgentEvent({ event, sequenceNumber, sourceKey, deviceRunSessionId, }) {
101
+ const type = ARGENT_EVENT_TYPE_TO_TYPE[event.type] ?? event.type;
102
+ const outcome = event.type === 'tool.completed'
103
+ ? 'success'
104
+ : event.type === 'tool.failed'
105
+ ? 'failure'
106
+ : undefined;
107
+ const data = {};
108
+ for (const [key, value] of Object.entries(event)) {
109
+ if (!NON_DATA_KEYS.has(key) && value !== undefined) {
110
+ data[key] = value;
111
+ }
112
+ }
113
+ data.sourceType = event.type;
114
+ if (typeof event.level === 'number') {
115
+ data.sourceLevel = event.level;
116
+ }
117
+ return {
118
+ v: 1,
119
+ // Consumers use eventId to deduplicate events across polls. Keep the per-file sequence
120
+ // monotonic across source-file truncations so an ID is never reused during collection.
121
+ eventId: `argent:${deviceRunSessionId}:${sourceKey}:${sequenceNumber}`,
122
+ ts: event.time,
123
+ producer: 'argent',
124
+ type,
125
+ ...(event.toolInvocationId ? { operationId: event.toolInvocationId } : {}),
126
+ ...(outcome ? { outcome } : {}),
127
+ ...(typeof event.durationMs === 'number' ? { durationMs: event.durationMs } : {}),
128
+ summary: event.msg,
129
+ data,
130
+ };
131
+ }
@@ -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 declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
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
- stateDir: string;
59
+ source: DeviceRunSessionEventSource;
19
60
  logger: bunyan;
20
61
  pollIntervalMs?: number;
21
62
  }): Promise<{
@@ -3,36 +3,16 @@ 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.startAgentDeviceEventCollectionAsync = startAgentDeviceEventCollectionAsync;
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 zod_1 = require("zod");
14
12
  const RemoteLoggerStream_1 = __importDefault(require("../../logging/RemoteLoggerStream"));
15
13
  const sentry_1 = require("../../sentry");
16
14
  const POLL_INTERVAL_MS = 1_000;
17
15
  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
16
  const CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
37
17
  mutation CreateDeviceRunSessionEventLogUploadSession($deviceRunSessionId: ID!) {
38
18
  deviceRunSession {
@@ -45,7 +25,8 @@ const CREATE_DEVICE_RUN_SESSION_EVENT_LOG_UPLOAD_SESSION_MUTATION = (0, gql_tada
45
25
  }
46
26
  }
47
27
  `);
48
- async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
28
+ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs = POLL_INTERVAL_MS, }) {
29
+ const { producer } = source;
49
30
  let didReportEventLogFailure = false;
50
31
  const reportEventLogFailure = (error, operation) => {
51
32
  if (didReportEventLogFailure) {
@@ -54,7 +35,7 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
54
35
  didReportEventLogFailure = true;
55
36
  sentry_1.Sentry.capture('Could not persist device run session events', error, {
56
37
  level: 'warning',
57
- tags: { phase: 'device-run-session-event-collection', operation },
38
+ tags: { phase: 'device-run-session-event-collection', operation, producer },
58
39
  extras: { deviceRunSessionId },
59
40
  });
60
41
  };
@@ -85,7 +66,7 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
85
66
  };
86
67
  let didReportCollectionFailure = false;
87
68
  const collectAsync = async () => {
88
- const eventFiles = await findAgentDeviceEventFilesAsync(stateDir);
69
+ const eventFiles = await source.findEventFilesAsync();
89
70
  await Promise.all(eventFiles.map(async (eventFile) => {
90
71
  const state = states.get(eventFile) ?? {
91
72
  offset: 0,
@@ -98,6 +79,7 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
98
79
  await collectEventFileAsync({
99
80
  eventFile,
100
81
  state,
82
+ source,
101
83
  deviceRunSessionId,
102
84
  writeEvent: event => eventLogStream.write(event),
103
85
  onParseFailure: ({ failure, lineNumber }) => {
@@ -106,10 +88,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
106
88
  if (parseFailureCount !== 1) {
107
89
  return;
108
90
  }
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', {
91
+ logger.warn({ producer, eventParseFailure: failure, lineNumber }, `Could not parse an ${producer} event log record.`);
92
+ sentry_1.Sentry.capture(`Could not parse an ${producer} event log record`, {
111
93
  level: 'warning',
112
- tags: { phase: 'agent-device-event-collection', reason: failure },
94
+ tags: { phase: `${producer}-event-collection`, reason: failure },
113
95
  extras: { deviceRunSessionId, lineNumber },
114
96
  });
115
97
  },
@@ -127,10 +109,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
127
109
  }
128
110
  didReportCollectionFailure = true;
129
111
  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, {
112
+ logger.warn({ err: error }, `Could not collect ${producer} events.`);
113
+ sentry_1.Sentry.capture(`Could not collect ${producer} events`, error, {
132
114
  level: 'warning',
133
- tags: { phase: 'agent-device-event-collection' },
115
+ tags: { phase: `${producer}-event-collection` },
134
116
  extras: { deviceRunSessionId },
135
117
  });
136
118
  }
@@ -150,10 +132,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
150
132
  })()
151
133
  .catch(err => {
152
134
  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, {
135
+ logger.warn({ err: error }, `Event collection poller for ${producer} failed.`);
136
+ sentry_1.Sentry.capture(`Event collection poller for ${producer} failed`, error, {
155
137
  level: 'warning',
156
- tags: { phase: 'agent-device-event-collection', operation: 'poll' },
138
+ tags: { phase: `${producer}-event-collection`, operation: 'poll' },
157
139
  extras: { deviceRunSessionId },
158
140
  });
159
141
  })
@@ -166,10 +148,10 @@ async function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, s
166
148
  await pollingPromise;
167
149
  await collectSafelyAsync();
168
150
  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', {
151
+ logger.warn({ producer, eventParseFailures: parseFailureCounts, parseFailureCount }, `Could not parse ${parseFailureCount} ${producer} event log records.`);
152
+ sentry_1.Sentry.capture(`Could not parse multiple ${producer} event log records`, {
171
153
  level: 'warning',
172
- tags: { phase: 'agent-device-event-collection' },
154
+ tags: { phase: `${producer}-event-collection` },
173
155
  extras: { deviceRunSessionId, parseFailureCount, parseFailureCounts },
174
156
  });
175
157
  }
@@ -199,23 +181,7 @@ async function createEventLogUploadSessionAsync(ctx, deviceRunSessionId) {
199
181
  headers: uploadSession.headers,
200
182
  };
201
183
  }
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, }) {
184
+ async function collectEventFileAsync({ eventFile, state, source, deviceRunSessionId, writeEvent, onParseFailure, }) {
219
185
  let fileSize;
220
186
  try {
221
187
  fileSize = (await node_fs_1.default.promises.stat(eventFile)).size;
@@ -234,6 +200,7 @@ async function collectEventFileAsync({ eventFile, state, deviceRunSessionId, wri
234
200
  if (fileSize === state.offset) {
235
201
  return;
236
202
  }
203
+ const sourceKey = source.sourceKeyForFile(eventFile);
237
204
  const handle = await node_fs_1.default.promises.open(eventFile, 'r');
238
205
  try {
239
206
  const buffer = new Uint8Array(new ArrayBuffer(fileSize - state.offset));
@@ -245,67 +212,22 @@ async function collectEventFileAsync({ eventFile, state, deviceRunSessionId, wri
245
212
  for (const line of lines) {
246
213
  const lineNumber = state.nextLineNumber++;
247
214
  const sequenceNumber = state.nextSequenceNumber++;
248
- const { event, failure } = parseAgentDeviceEvent(line);
215
+ const { event, failure } = source.parseLine({
216
+ line,
217
+ sourceKey,
218
+ sequenceNumber,
219
+ deviceRunSessionId,
220
+ });
249
221
  if (failure) {
250
222
  onParseFailure({ failure, lineNumber });
251
223
  }
252
224
  if (!event) {
253
225
  continue;
254
226
  }
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);
227
+ writeEvent(event);
262
228
  }
263
229
  }
264
230
  finally {
265
231
  await handle.close();
266
232
  }
267
233
  }
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.3.0",
3
+ "version": "21.4.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.3.0",
42
+ "@expo/eas-build-job": "21.4.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.3.0",
49
+ "@expo/steps": "21.4.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": "3784a503997fe6100c26034b9484295b09646214"
103
+ "gitHead": "dcc7201ca87cb28535990e28856409e91baa41fe"
104
104
  }