@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.
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +2 -2
- package/dist/steps/functions/startArgentRemoteSession.d.ts +8 -1
- package/dist/steps/functions/startArgentRemoteSession.js +35 -2
- package/dist/steps/utils/agentDeviceEvents.d.ts +11 -0
- package/dist/steps/utils/agentDeviceEvents.js +116 -0
- 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 +27 -105
- package/package.json +4 -4
|
@@ -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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,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.
|
|
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
|
|
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
|
|
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({
|
|
110
|
-
sentry_1.Sentry.capture(
|
|
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:
|
|
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 },
|
|
131
|
-
sentry_1.Sentry.capture(
|
|
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:
|
|
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 },
|
|
154
|
-
sentry_1.Sentry.capture(
|
|
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:
|
|
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({
|
|
170
|
-
sentry_1.Sentry.capture(
|
|
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:
|
|
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
|
|
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 } =
|
|
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
|
-
|
|
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
|
+
"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.
|
|
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.
|
|
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": "
|
|
103
|
+
"gitHead": "dcc7201ca87cb28535990e28856409e91baa41fe"
|
|
104
104
|
}
|