@expo/build-tools 20.4.0 → 21.0.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/gradle.d.ts +5 -0
- package/dist/android/gradle.js +13 -33
- package/dist/common/installDependencies.js +21 -18
- package/dist/common/projectSources.js +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/context.js +31 -1
- package/dist/ios/fastlane.d.ts +1 -1
- package/dist/steps/easFunctions.js +18 -0
- package/dist/steps/functions/capturePosthogEvent.d.ts +2 -0
- package/dist/steps/functions/capturePosthogEvent.js +79 -0
- package/dist/steps/functions/createPosthogAnnotation.d.ts +2 -0
- package/dist/steps/functions/createPosthogAnnotation.js +75 -0
- package/dist/steps/functions/createSubmissionEntity.js +25 -1
- package/dist/steps/functions/downloadArtifact.js +13 -3
- package/dist/steps/functions/finishIosSimulatorRecordings.d.ts +2 -0
- package/dist/steps/functions/finishIosSimulatorRecordings.js +24 -0
- package/dist/steps/functions/repack.js +12 -7
- package/dist/steps/functions/restoreCache.js +2 -1
- package/dist/steps/functions/rolloutPosthogFlag.d.ts +2 -0
- package/dist/steps/functions/rolloutPosthogFlag.js +208 -0
- package/dist/steps/functions/saveCache.js +3 -2
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +21 -8
- package/dist/steps/functions/startArgentRemoteSession.d.ts +14 -0
- package/dist/steps/functions/startArgentRemoteSession.js +105 -44
- package/dist/steps/functions/startIosSimulator.js +19 -0
- package/dist/steps/functions/startIosSimulatorRecordings.d.ts +2 -0
- package/dist/steps/functions/startIosSimulatorRecordings.js +20 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +7 -5
- package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.d.ts +3 -0
- package/dist/steps/functions/uploadDeviceRunSessionScreenRecordings.js +91 -0
- package/dist/steps/functions/uploadPosthogSourcemaps.d.ts +2 -0
- package/dist/steps/functions/uploadPosthogSourcemaps.js +83 -0
- package/dist/steps/functions/waitForPosthogMetric.d.ts +2 -0
- package/dist/steps/functions/waitForPosthogMetric.js +135 -0
- package/dist/steps/functions/waitForPosthogQuery.d.ts +2 -0
- package/dist/steps/functions/waitForPosthogQuery.js +95 -0
- package/dist/steps/utils/IosSimulatorRecordingUtils.d.ts +17 -0
- package/dist/steps/utils/IosSimulatorRecordingUtils.js +218 -0
- package/dist/steps/utils/PosthogClient.d.ts +46 -0
- package/dist/steps/utils/PosthogClient.js +156 -0
- package/dist/steps/utils/PosthogUtils.d.ts +41 -0
- package/dist/steps/utils/PosthogUtils.js +65 -0
- package/dist/steps/utils/agentDeviceArtifacts.d.ts +27 -0
- package/dist/steps/utils/agentDeviceArtifacts.js +133 -0
- package/dist/steps/utils/android/gradle.d.ts +5 -0
- package/dist/steps/utils/android/gradle.js +13 -62
- package/dist/steps/utils/argentArtifacts.d.ts +5 -3
- package/dist/steps/utils/argentArtifacts.js +95 -23
- package/dist/steps/utils/deviceRunSessionArtifacts.d.ts +3 -1
- package/dist/steps/utils/deviceRunSessionArtifacts.js +6 -2
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +8 -0
- package/dist/steps/utils/remoteDeviceRunSession.js +62 -1
- package/dist/utils/AndroidEmulatorUtils.js +3 -0
- package/dist/utils/IosSimulatorUtils.d.ts +5 -0
- package/dist/utils/IosSimulatorUtils.js +81 -2
- package/dist/utils/expoUpdates.js +6 -1
- package/dist/utils/expoUpdatesEmbedded.js +4 -0
- package/dist/utils/processes.d.ts +1 -0
- package/dist/utils/processes.js +23 -0
- package/package.json +9 -8
- package/resources/record-sim/Package.swift +28 -0
- package/resources/record-sim/README.md +79 -0
- package/resources/record-sim/Sources/RecordSim/FramebufferDisplaySource.swift +192 -0
- package/resources/record-sim/Sources/RecordSim/PrivateSimulatorServices.swift +96 -0
- package/resources/record-sim/Sources/RecordSim/RecordingModels.swift +70 -0
- package/resources/record-sim/Sources/RecordSim/RecordingOutputWriter.swift +136 -0
- package/resources/record-sim/Sources/RecordSim/SimulatorRecorder.swift +632 -0
- package/resources/record-sim/Sources/RecordSim/Utilities.swift +60 -0
- package/resources/record-sim/Sources/record-sim/main.swift +143 -0
|
@@ -0,0 +1,91 @@
|
|
|
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.createUploadDeviceRunSessionScreenRecordingsBuildFunction = createUploadDeviceRunSessionScreenRecordingsBuildFunction;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
|
+
const steps_1 = require("@expo/steps");
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const promise_limit_1 = __importDefault(require("promise-limit"));
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
const sentry_1 = require("../../sentry");
|
|
15
|
+
const artifacts_1 = require("../../utils/artifacts");
|
|
16
|
+
const deviceRunSessionArtifacts_1 = require("../utils/deviceRunSessionArtifacts");
|
|
17
|
+
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
18
|
+
const RecordingsSchema = zod_1.z.array(zod_1.z.object({
|
|
19
|
+
udid: zod_1.z.string(),
|
|
20
|
+
deviceName: zod_1.z.string(),
|
|
21
|
+
runtimeDisplayName: zod_1.z.string(),
|
|
22
|
+
directory: zod_1.z.string(),
|
|
23
|
+
}));
|
|
24
|
+
const RecordingManifestSchema = zod_1.z.object({
|
|
25
|
+
firstFrameWallClock: zod_1.z.object({
|
|
26
|
+
iso8601: zod_1.z.string(),
|
|
27
|
+
}),
|
|
28
|
+
recording: zod_1.z.string(),
|
|
29
|
+
});
|
|
30
|
+
function createUploadDeviceRunSessionScreenRecordingsBuildFunction(ctx) {
|
|
31
|
+
return new steps_1.BuildFunction({
|
|
32
|
+
namespace: 'eas',
|
|
33
|
+
id: 'upload_device_run_session_screen_recordings',
|
|
34
|
+
name: 'Upload device run session screen recordings',
|
|
35
|
+
__metricsId: 'eas/upload_device_run_session_screen_recordings',
|
|
36
|
+
supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
|
|
37
|
+
inputProviders: [
|
|
38
|
+
steps_1.BuildStepInput.createProvider({
|
|
39
|
+
id: 'recordings_json',
|
|
40
|
+
required: false,
|
|
41
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.JSON,
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
fn: async ({ logger }, { env, inputs }) => {
|
|
45
|
+
const result = RecordingsSchema.safeParse(inputs.recordings_json.value ?? []);
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
throw new eas_build_job_1.SystemError('Invalid iOS Simulator recordings input.', {
|
|
48
|
+
cause: result.error,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const recordings = result.data;
|
|
52
|
+
if (recordings.length === 0) {
|
|
53
|
+
logger.info('No iOS Simulator recordings found; skipping uploads.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
57
|
+
const limit = (0, promise_limit_1.default)(5);
|
|
58
|
+
await Promise.all(recordings.map(recording => limit(async () => {
|
|
59
|
+
const displayName = `${recording.deviceName} screen recording`;
|
|
60
|
+
try {
|
|
61
|
+
const metadata = RecordingManifestSchema.parse(JSON.parse(await (0, promises_1.readFile)(node_path_1.default.join(recording.directory, 'session.json'), 'utf-8')));
|
|
62
|
+
const recordingPath = node_path_1.default.join(recording.directory, metadata.recording);
|
|
63
|
+
const { size } = await (0, promises_1.stat)(recordingPath);
|
|
64
|
+
const recordingId = node_path_1.default.basename(recording.directory);
|
|
65
|
+
logger.info(`Uploading screen recording for ${recording.deviceName} (${(0, artifacts_1.formatBytes)(size)}).`);
|
|
66
|
+
await (0, deviceRunSessionArtifacts_1.uploadDeviceRunSessionArtifactAsync)(ctx, {
|
|
67
|
+
deviceRunSessionId,
|
|
68
|
+
artifactId: recordingId,
|
|
69
|
+
name: displayName,
|
|
70
|
+
filename: `${recordingId}.mp4`,
|
|
71
|
+
kind: 'screen-recording',
|
|
72
|
+
metadata: {
|
|
73
|
+
__eas_screen_recording: '1',
|
|
74
|
+
udid: recording.udid,
|
|
75
|
+
deviceName: recording.deviceName,
|
|
76
|
+
runtimeDisplayName: recording.runtimeDisplayName,
|
|
77
|
+
firstFrameAt: metadata.firstFrameWallClock.iso8601,
|
|
78
|
+
},
|
|
79
|
+
size,
|
|
80
|
+
stream: (0, node_fs_1.createReadStream)(recordingPath),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
85
|
+
sentry_1.Sentry.capture('Could not upload iOS Simulator screen recording', error);
|
|
86
|
+
logger.warn({ err: error }, `Could not upload screen recording for ${recording.deviceName}.`);
|
|
87
|
+
}
|
|
88
|
+
})));
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.createUploadPosthogSourcemapsFunction = createUploadPosthogSourcemapsFunction;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
|
+
const logger_1 = require("@expo/logger");
|
|
9
|
+
const steps_1 = require("@expo/steps");
|
|
10
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
11
|
+
const PosthogClient_1 = require("../utils/PosthogClient");
|
|
12
|
+
const PosthogUtils_1 = require("../utils/PosthogUtils");
|
|
13
|
+
function createUploadPosthogSourcemapsFunction() {
|
|
14
|
+
return new steps_1.BuildFunction({
|
|
15
|
+
namespace: 'eas',
|
|
16
|
+
id: 'posthog_upload_sourcemaps',
|
|
17
|
+
name: 'Upload source maps to PostHog',
|
|
18
|
+
__metricsId: 'eas/posthog_upload_sourcemaps',
|
|
19
|
+
inputProviders: [
|
|
20
|
+
steps_1.BuildStepInput.createProvider({
|
|
21
|
+
id: 'directory',
|
|
22
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
23
|
+
required: false,
|
|
24
|
+
defaultValue: 'dist',
|
|
25
|
+
}),
|
|
26
|
+
steps_1.BuildStepInput.createProvider({
|
|
27
|
+
id: 'api_key',
|
|
28
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
29
|
+
required: false,
|
|
30
|
+
}),
|
|
31
|
+
steps_1.BuildStepInput.createProvider({
|
|
32
|
+
id: 'project_id',
|
|
33
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
34
|
+
required: false,
|
|
35
|
+
}),
|
|
36
|
+
steps_1.BuildStepInput.createProvider({
|
|
37
|
+
id: 'ignore_error',
|
|
38
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
|
|
39
|
+
required: false,
|
|
40
|
+
}),
|
|
41
|
+
],
|
|
42
|
+
fn: async (stepCtx, { inputs, env, signal }) => {
|
|
43
|
+
const { logger } = stepCtx;
|
|
44
|
+
const ignoreError = Boolean(inputs.ignore_error.value);
|
|
45
|
+
const result = PosthogClient_1.PosthogClient.fromEnv({
|
|
46
|
+
apiKeyOverride: inputs.api_key.value,
|
|
47
|
+
projectIdOverride: inputs.project_id.value,
|
|
48
|
+
env,
|
|
49
|
+
});
|
|
50
|
+
if (!result.client) {
|
|
51
|
+
PosthogUtils_1.PosthogUtils.failOrLogError({
|
|
52
|
+
logger,
|
|
53
|
+
ignoreError,
|
|
54
|
+
error: PosthogUtils_1.PosthogUtils.missingCredentialsError(result.missing),
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const client = result.client;
|
|
59
|
+
const directory = inputs.directory.value;
|
|
60
|
+
const { host, env: cliEnv } = client.cliConfig();
|
|
61
|
+
logger.info(`Uploading source maps from "${directory}" to PostHog`);
|
|
62
|
+
try {
|
|
63
|
+
await (0, turtle_spawn_1.default)('npx', ['-y', '@posthog/cli', '--host', host, 'hermes', 'upload', '--directory', directory], {
|
|
64
|
+
logger,
|
|
65
|
+
// @posthog/cli writes all its logs, progress and success included, to stderr; tag them as stdout so they don't surface as build errors.
|
|
66
|
+
mode: logger_1.PipeMode.COMBINED_AS_STDOUT,
|
|
67
|
+
cwd: stepCtx.workingDirectory,
|
|
68
|
+
env: { ...env, ...cliEnv },
|
|
69
|
+
signal,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
PosthogUtils_1.PosthogUtils.failOrLogError({
|
|
74
|
+
logger,
|
|
75
|
+
ignoreError,
|
|
76
|
+
error: new eas_build_job_1.UserError('EAS_POSTHOG_SOURCEMAPS_FAILED', 'Uploading source maps to PostHog failed.', { cause: error }),
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
logger.info('Uploaded source maps to PostHog');
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWaitForPosthogMetricFunction = createWaitForPosthogMetricFunction;
|
|
4
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
|
+
const steps_1 = require("@expo/steps");
|
|
6
|
+
const PosthogClient_1 = require("../utils/PosthogClient");
|
|
7
|
+
const PosthogUtils_1 = require("../utils/PosthogUtils");
|
|
8
|
+
const OPERATORS = {
|
|
9
|
+
lt: { symbol: '<', test: (value, threshold) => value < threshold },
|
|
10
|
+
lte: { symbol: '<=', test: (value, threshold) => value <= threshold },
|
|
11
|
+
gt: { symbol: '>', test: (value, threshold) => value > threshold },
|
|
12
|
+
gte: { symbol: '>=', test: (value, threshold) => value >= threshold },
|
|
13
|
+
eq: { symbol: '=', test: (value, threshold) => value === threshold },
|
|
14
|
+
};
|
|
15
|
+
function createWaitForPosthogMetricFunction() {
|
|
16
|
+
return new steps_1.BuildFunction({
|
|
17
|
+
namespace: 'eas',
|
|
18
|
+
id: 'posthog_wait_for_metric',
|
|
19
|
+
name: 'Wait for a PostHog metric',
|
|
20
|
+
__metricsId: 'eas/posthog_wait_for_metric',
|
|
21
|
+
inputProviders: [
|
|
22
|
+
steps_1.BuildStepInput.createProvider({
|
|
23
|
+
id: 'query',
|
|
24
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
25
|
+
required: true,
|
|
26
|
+
}),
|
|
27
|
+
steps_1.BuildStepInput.createProvider({
|
|
28
|
+
id: 'operator',
|
|
29
|
+
allowedValues: Object.keys(OPERATORS),
|
|
30
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
31
|
+
required: true,
|
|
32
|
+
}),
|
|
33
|
+
steps_1.BuildStepInput.createProvider({
|
|
34
|
+
id: 'threshold',
|
|
35
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
36
|
+
required: true,
|
|
37
|
+
}),
|
|
38
|
+
steps_1.BuildStepInput.createProvider({
|
|
39
|
+
id: 'timeout_seconds',
|
|
40
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
41
|
+
required: false,
|
|
42
|
+
defaultValue: PosthogUtils_1.PosthogUtils.DEFAULT_POLL_TIMEOUT_SECONDS,
|
|
43
|
+
}),
|
|
44
|
+
steps_1.BuildStepInput.createProvider({
|
|
45
|
+
id: 'interval_seconds',
|
|
46
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
47
|
+
required: false,
|
|
48
|
+
defaultValue: PosthogUtils_1.PosthogUtils.DEFAULT_POLL_INTERVAL_SECONDS,
|
|
49
|
+
}),
|
|
50
|
+
steps_1.BuildStepInput.createProvider({
|
|
51
|
+
id: 'api_key',
|
|
52
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
53
|
+
required: false,
|
|
54
|
+
}),
|
|
55
|
+
steps_1.BuildStepInput.createProvider({
|
|
56
|
+
id: 'project_id',
|
|
57
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
58
|
+
required: false,
|
|
59
|
+
}),
|
|
60
|
+
],
|
|
61
|
+
outputProviders: [steps_1.BuildStepOutput.createProvider({ id: 'value', required: false })],
|
|
62
|
+
fn: async (stepCtx, { inputs, outputs, env, signal }) => {
|
|
63
|
+
const { logger } = stepCtx;
|
|
64
|
+
const operator = inputs.operator.value;
|
|
65
|
+
if (!Object.keys(OPERATORS).includes(operator)) {
|
|
66
|
+
throw new eas_build_job_1.UserError('EAS_POSTHOG_METRIC_INVALID_OPERATOR', `Invalid "operator" "${operator}". Must be one of: ${Object.keys(OPERATORS).join(', ')}.`);
|
|
67
|
+
}
|
|
68
|
+
const timeoutSeconds = inputs.timeout_seconds.value;
|
|
69
|
+
const intervalSeconds = inputs.interval_seconds.value;
|
|
70
|
+
PosthogUtils_1.PosthogUtils.assertPollBoundsPositive({
|
|
71
|
+
timeoutSeconds,
|
|
72
|
+
intervalSeconds,
|
|
73
|
+
errorCode: 'EAS_POSTHOG_METRIC_INVALID_INTERVAL',
|
|
74
|
+
});
|
|
75
|
+
const result = PosthogClient_1.PosthogClient.fromEnv({
|
|
76
|
+
apiKeyOverride: inputs.api_key.value,
|
|
77
|
+
projectIdOverride: inputs.project_id.value,
|
|
78
|
+
env,
|
|
79
|
+
});
|
|
80
|
+
if (!result.client) {
|
|
81
|
+
throw PosthogUtils_1.PosthogUtils.missingCredentialsError(result.missing);
|
|
82
|
+
}
|
|
83
|
+
const client = result.client;
|
|
84
|
+
const value = await waitForPosthogMetricAsync({
|
|
85
|
+
logger,
|
|
86
|
+
client,
|
|
87
|
+
query: inputs.query.value,
|
|
88
|
+
operator: operator,
|
|
89
|
+
threshold: inputs.threshold.value,
|
|
90
|
+
timeoutSeconds,
|
|
91
|
+
intervalSeconds,
|
|
92
|
+
signal,
|
|
93
|
+
});
|
|
94
|
+
outputs.value.set(String(value));
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function waitForPosthogMetricAsync({ logger, client, query, operator, threshold, timeoutSeconds, intervalSeconds, signal, }) {
|
|
99
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
100
|
+
const { symbol, test } = OPERATORS[operator];
|
|
101
|
+
const target = `${symbol} ${threshold}`;
|
|
102
|
+
let lastValue;
|
|
103
|
+
logger.info(`Waiting for the PostHog metric to reach ${target}. Checking every ${intervalSeconds}s for up to ${timeoutSeconds}s.`);
|
|
104
|
+
for (;;) {
|
|
105
|
+
let value = undefined;
|
|
106
|
+
try {
|
|
107
|
+
value = await queryMetricAsync({ logger, client, query, signal });
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (!(error instanceof PosthogClient_1.PosthogRetryableError)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (value !== undefined) {
|
|
115
|
+
lastValue = value;
|
|
116
|
+
if (test(value, threshold)) {
|
|
117
|
+
logger.info(`Metric is ${value}. Target ${target} met.`);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
logger.info(`Metric is ${value}. Still waiting for ${target}.`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
logger.info('The query has not returned a numeric value yet. Still waiting.');
|
|
124
|
+
}
|
|
125
|
+
if (!(await PosthogUtils_1.PosthogUtils.waitForNextPollAsync({ intervalSeconds, deadline, signal }))) {
|
|
126
|
+
throw new eas_build_job_1.UserError('EAS_POSTHOG_METRIC_TIMEOUT', lastValue !== undefined
|
|
127
|
+
? `The PostHog metric did not reach ${target} within ${timeoutSeconds}s (last value: ${lastValue}).`
|
|
128
|
+
: `The PostHog query never returned a numeric value within ${timeoutSeconds}s. Check that the query returns a single number and that PostHog is reachable.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function queryMetricAsync({ logger, client, query, signal, }) {
|
|
133
|
+
const cell = await client.runQueryAsync(query, logger, signal);
|
|
134
|
+
return typeof cell === 'number' && Number.isFinite(cell) ? cell : undefined;
|
|
135
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWaitForPosthogQueryFunction = createWaitForPosthogQueryFunction;
|
|
4
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
5
|
+
const steps_1 = require("@expo/steps");
|
|
6
|
+
const PosthogClient_1 = require("../utils/PosthogClient");
|
|
7
|
+
const PosthogUtils_1 = require("../utils/PosthogUtils");
|
|
8
|
+
function createWaitForPosthogQueryFunction() {
|
|
9
|
+
return new steps_1.BuildFunction({
|
|
10
|
+
namespace: 'eas',
|
|
11
|
+
id: 'posthog_wait_for_query',
|
|
12
|
+
name: 'Wait for a PostHog query',
|
|
13
|
+
__metricsId: 'eas/posthog_wait_for_query',
|
|
14
|
+
inputProviders: [
|
|
15
|
+
steps_1.BuildStepInput.createProvider({
|
|
16
|
+
id: 'query',
|
|
17
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
steps_1.BuildStepInput.createProvider({
|
|
21
|
+
id: 'timeout_seconds',
|
|
22
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
23
|
+
required: false,
|
|
24
|
+
defaultValue: PosthogUtils_1.PosthogUtils.DEFAULT_POLL_TIMEOUT_SECONDS,
|
|
25
|
+
}),
|
|
26
|
+
steps_1.BuildStepInput.createProvider({
|
|
27
|
+
id: 'interval_seconds',
|
|
28
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
29
|
+
required: false,
|
|
30
|
+
defaultValue: PosthogUtils_1.PosthogUtils.DEFAULT_POLL_INTERVAL_SECONDS,
|
|
31
|
+
}),
|
|
32
|
+
steps_1.BuildStepInput.createProvider({
|
|
33
|
+
id: 'api_key',
|
|
34
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
35
|
+
required: false,
|
|
36
|
+
}),
|
|
37
|
+
steps_1.BuildStepInput.createProvider({
|
|
38
|
+
id: 'project_id',
|
|
39
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
40
|
+
required: false,
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
fn: async (stepCtx, { inputs, env, signal }) => {
|
|
44
|
+
const { logger } = stepCtx;
|
|
45
|
+
const timeoutSeconds = inputs.timeout_seconds.value;
|
|
46
|
+
const intervalSeconds = inputs.interval_seconds.value;
|
|
47
|
+
PosthogUtils_1.PosthogUtils.assertPollBoundsPositive({
|
|
48
|
+
timeoutSeconds,
|
|
49
|
+
intervalSeconds,
|
|
50
|
+
errorCode: 'EAS_POSTHOG_QUERY_INVALID_INTERVAL',
|
|
51
|
+
});
|
|
52
|
+
const result = PosthogClient_1.PosthogClient.fromEnv({
|
|
53
|
+
apiKeyOverride: inputs.api_key.value,
|
|
54
|
+
projectIdOverride: inputs.project_id.value,
|
|
55
|
+
env,
|
|
56
|
+
});
|
|
57
|
+
if (!result.client) {
|
|
58
|
+
throw PosthogUtils_1.PosthogUtils.missingCredentialsError(result.missing);
|
|
59
|
+
}
|
|
60
|
+
const client = result.client;
|
|
61
|
+
await waitForPosthogQueryAsync({
|
|
62
|
+
logger,
|
|
63
|
+
client,
|
|
64
|
+
query: inputs.query.value,
|
|
65
|
+
timeoutSeconds,
|
|
66
|
+
intervalSeconds,
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function waitForPosthogQueryAsync({ logger, client, query, timeoutSeconds, intervalSeconds, signal, }) {
|
|
73
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
74
|
+
logger.info(`Waiting for the PostHog query to return true. Checking every ${intervalSeconds}s for up to ${timeoutSeconds}s.`);
|
|
75
|
+
for (;;) {
|
|
76
|
+
let cell;
|
|
77
|
+
try {
|
|
78
|
+
cell = await client.runQueryAsync(query, logger, signal);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof PosthogClient_1.PosthogRetryableError)) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
cell = undefined;
|
|
85
|
+
}
|
|
86
|
+
if (cell === true || (typeof cell === 'number' && cell !== 0)) {
|
|
87
|
+
logger.info('Query returned true.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
logger.info('Query is not true yet. Still waiting.');
|
|
91
|
+
if (!(await PosthogUtils_1.PosthogUtils.waitForNextPollAsync({ intervalSeconds, deadline, signal }))) {
|
|
92
|
+
throw new eas_build_job_1.UserError('EAS_POSTHOG_QUERY_TIMEOUT', `The PostHog query did not return true within ${timeoutSeconds}s. It must select a single boolean, e.g. "SELECT count() > 100 FROM events".`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Env } from '@expo/eas-build-job';
|
|
2
|
+
import { type bunyan } from '@expo/logger';
|
|
3
|
+
import { type IosSimulatorUuid } from '../../utils/IosSimulatorUtils';
|
|
4
|
+
export declare namespace IosSimulatorRecordingUtils {
|
|
5
|
+
function startAsync({ env, logger }: {
|
|
6
|
+
env: Env;
|
|
7
|
+
logger: bunyan;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
function finishAsync({ logger }: {
|
|
10
|
+
logger: bunyan;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
udid: IosSimulatorUuid;
|
|
13
|
+
deviceName: string;
|
|
14
|
+
runtimeDisplayName: string;
|
|
15
|
+
directory: string;
|
|
16
|
+
}[]>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
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.IosSimulatorRecordingUtils = void 0;
|
|
7
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const promises_1 = require("node:fs/promises");
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const promises_2 = require("node:timers/promises");
|
|
13
|
+
const sentry_1 = require("../../sentry");
|
|
14
|
+
const IosSimulatorUtils_1 = require("../../utils/IosSimulatorUtils");
|
|
15
|
+
const IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS = 2_000;
|
|
16
|
+
const RECORD_SIM_FINISH_TIMEOUT_MS = 30_000;
|
|
17
|
+
const RECORD_SIM_FORCE_STOP_TIMEOUT_MS = 5_000;
|
|
18
|
+
const RECORD_SIM_MAX_ATTEMPTS_PER_BOOT = 3;
|
|
19
|
+
const RECORD_SIM_COMMAND = 'record-sim';
|
|
20
|
+
let activeIosSimulatorRecordingSession = null;
|
|
21
|
+
var IosSimulatorRecordingUtils;
|
|
22
|
+
(function (IosSimulatorRecordingUtils) {
|
|
23
|
+
async function startAsync({ env, logger }) {
|
|
24
|
+
if (activeIosSimulatorRecordingSession) {
|
|
25
|
+
logger.info('iOS Simulator screen recording polling is already running.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const recordSimCommand = await resolveRecordSimCommandAsync({ env });
|
|
29
|
+
if (!recordSimCommand) {
|
|
30
|
+
logger.warn('record-sim binary is not available; iOS Simulator screen recordings are disabled.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const recordingsRootDirectory = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-simulator-recordings-'));
|
|
34
|
+
const session = {
|
|
35
|
+
env,
|
|
36
|
+
logger,
|
|
37
|
+
recordSimCommand,
|
|
38
|
+
recordingsRootDirectory,
|
|
39
|
+
activeRecordings: new Map(),
|
|
40
|
+
completedRecordings: [],
|
|
41
|
+
recordingFailureCounts: new Map(),
|
|
42
|
+
pollingPromise: Promise.resolve(),
|
|
43
|
+
abortController: new AbortController(),
|
|
44
|
+
};
|
|
45
|
+
activeIosSimulatorRecordingSession = session;
|
|
46
|
+
logger.info('Started polling iOS Simulators for screen recordings.');
|
|
47
|
+
session.pollingPromise = pollIosSimulatorRecordingsAsync(session).catch(err => {
|
|
48
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
49
|
+
sentry_1.Sentry.capture('iOS Simulator screen recording poller failed', error);
|
|
50
|
+
logger.warn({ err: error }, 'iOS Simulator screen recording poller failed.');
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
IosSimulatorRecordingUtils.startAsync = startAsync;
|
|
54
|
+
async function finishAsync({ logger }) {
|
|
55
|
+
const session = activeIosSimulatorRecordingSession;
|
|
56
|
+
if (!session) {
|
|
57
|
+
logger.info('No iOS Simulator screen recordings are running.');
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
activeIosSimulatorRecordingSession = null;
|
|
61
|
+
session.abortController.abort();
|
|
62
|
+
await session.pollingPromise;
|
|
63
|
+
await Promise.all([...session.activeRecordings.values()].map(async (recording) => {
|
|
64
|
+
logger.info(`Stopping screen recording for ${recording.deviceName}.`);
|
|
65
|
+
recording.recordingProcess.kill('SIGINT');
|
|
66
|
+
const finished = await Promise.race([
|
|
67
|
+
recording.completionPromise.then(() => true),
|
|
68
|
+
(0, promises_2.setTimeout)(RECORD_SIM_FINISH_TIMEOUT_MS, false, { ref: false }),
|
|
69
|
+
]);
|
|
70
|
+
if (finished) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
logger.warn(`Forcing iOS Simulator recording process for ${recording.deviceName} to stop.`);
|
|
74
|
+
recording.recordingProcess.kill('SIGKILL');
|
|
75
|
+
const killed = await Promise.race([
|
|
76
|
+
recording.completionPromise.then(() => true),
|
|
77
|
+
(0, promises_2.setTimeout)(RECORD_SIM_FORCE_STOP_TIMEOUT_MS, false, { ref: false }),
|
|
78
|
+
]);
|
|
79
|
+
if (!killed) {
|
|
80
|
+
logger.warn(`iOS Simulator recording process for ${recording.deviceName} did not exit after SIGKILL.`);
|
|
81
|
+
sentry_1.Sentry.capture(`iOS Simulator recording process for ${recording.deviceName} did not exit after SIGKILL.`, {
|
|
82
|
+
extras: {
|
|
83
|
+
output: recording.getOutput(),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
const completedRecordings = [...session.completedRecordings].sort((a, b) => a.startedAt.getTime() - b.startedAt.getTime());
|
|
89
|
+
return completedRecordings.map(recording => ({
|
|
90
|
+
udid: recording.udid,
|
|
91
|
+
deviceName: recording.deviceName,
|
|
92
|
+
runtimeDisplayName: recording.runtimeDisplayName,
|
|
93
|
+
directory: recording.outputDirectory,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
IosSimulatorRecordingUtils.finishAsync = finishAsync;
|
|
97
|
+
})(IosSimulatorRecordingUtils || (exports.IosSimulatorRecordingUtils = IosSimulatorRecordingUtils = {}));
|
|
98
|
+
async function pollIosSimulatorRecordingsAsync(session) {
|
|
99
|
+
let listDevicesErrorCount = 0;
|
|
100
|
+
const signal = session.abortController.signal;
|
|
101
|
+
while (!signal.aborted) {
|
|
102
|
+
try {
|
|
103
|
+
const bootedDevices = await IosSimulatorUtils_1.IosSimulatorUtils.getAvailableDevicesAsync({
|
|
104
|
+
env: session.env,
|
|
105
|
+
filter: 'booted',
|
|
106
|
+
});
|
|
107
|
+
if (signal.aborted) {
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
listDevicesErrorCount = 0;
|
|
111
|
+
const bootedUdids = new Set(bootedDevices.map(device => device.udid));
|
|
112
|
+
for (const udid of session.recordingFailureCounts.keys()) {
|
|
113
|
+
if (!bootedUdids.has(udid)) {
|
|
114
|
+
session.recordingFailureCounts.delete(udid);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const device of bootedDevices) {
|
|
118
|
+
if (session.activeRecordings.has(device.udid) ||
|
|
119
|
+
(session.recordingFailureCounts.get(device.udid) ?? 0) >= RECORD_SIM_MAX_ATTEMPTS_PER_BOOT) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
await startIosSimulatorRecordingAsync(session, {
|
|
123
|
+
udid: device.udid,
|
|
124
|
+
deviceName: device.name,
|
|
125
|
+
runtimeDisplayName: device.runtimeDisplayName,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
131
|
+
listDevicesErrorCount += 1;
|
|
132
|
+
if (listDevicesErrorCount === 1 || listDevicesErrorCount % 5 === 0) {
|
|
133
|
+
sentry_1.Sentry.capture('Could not poll iOS Simulators for screen recordings', error);
|
|
134
|
+
session.logger.warn({ err: error, failedSimulatorListCount: listDevicesErrorCount }, 'Could not poll iOS Simulators for screen recordings.');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (!signal.aborted) {
|
|
138
|
+
try {
|
|
139
|
+
await (0, promises_2.setTimeout)(IOS_SIMULATOR_RECORDING_POLL_INTERVAL_MS, undefined, {
|
|
140
|
+
signal,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (!signal.aborted) {
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function startIosSimulatorRecordingAsync(session, { udid, deviceName, runtimeDisplayName, }) {
|
|
152
|
+
const startedAt = new Date();
|
|
153
|
+
const recordingId = (0, node_crypto_1.randomUUID)();
|
|
154
|
+
const outputDirectory = node_path_1.default.join(session.recordingsRootDirectory, recordingId);
|
|
155
|
+
await (0, promises_1.mkdir)(outputDirectory, { recursive: true });
|
|
156
|
+
session.logger.info(`Starting screen recording for ${deviceName}.`);
|
|
157
|
+
const recordingSpawn = (0, turtle_spawn_1.default)(session.recordSimCommand, ['--udid', udid, '--output', outputDirectory, '--segment-duration', '0'], {
|
|
158
|
+
env: session.env,
|
|
159
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
160
|
+
});
|
|
161
|
+
const getOutput = captureProcessOutput(recordingSpawn.child);
|
|
162
|
+
const completionPromise = recordingSpawn
|
|
163
|
+
.then(() => undefined)
|
|
164
|
+
.catch((err) => {
|
|
165
|
+
session.recordingFailureCounts.set(udid, (session.recordingFailureCounts.get(udid) ?? 0) + 1);
|
|
166
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
167
|
+
sentry_1.Sentry.capture('iOS Simulator screen recording process failed', error);
|
|
168
|
+
session.logger.warn({ err: error, recordSimOutput: getOutput() }, `Screen recording process failed for ${deviceName}.`);
|
|
169
|
+
})
|
|
170
|
+
.finally(() => {
|
|
171
|
+
session.activeRecordings.delete(udid);
|
|
172
|
+
session.completedRecordings.push({
|
|
173
|
+
id: recordingId,
|
|
174
|
+
udid,
|
|
175
|
+
deviceName,
|
|
176
|
+
runtimeDisplayName,
|
|
177
|
+
outputDirectory,
|
|
178
|
+
startedAt,
|
|
179
|
+
getOutput,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
session.activeRecordings.set(udid, {
|
|
183
|
+
id: recordingId,
|
|
184
|
+
udid,
|
|
185
|
+
deviceName,
|
|
186
|
+
runtimeDisplayName,
|
|
187
|
+
outputDirectory,
|
|
188
|
+
recordingProcess: recordingSpawn.child,
|
|
189
|
+
completionPromise,
|
|
190
|
+
startedAt,
|
|
191
|
+
getOutput,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async function resolveRecordSimCommandAsync({ env }) {
|
|
195
|
+
try {
|
|
196
|
+
await (0, turtle_spawn_1.default)('which', [RECORD_SIM_COMMAND], { env });
|
|
197
|
+
return RECORD_SIM_COMMAND;
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
const packagedRecordSimPath = node_path_1.default.join(__dirname, '..', '..', '..', 'bin', RECORD_SIM_COMMAND);
|
|
201
|
+
try {
|
|
202
|
+
await (0, promises_1.access)(packagedRecordSimPath);
|
|
203
|
+
return packagedRecordSimPath;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function captureProcessOutput(recordingProcess) {
|
|
210
|
+
let output = '';
|
|
211
|
+
const appendChunk = (chunk) => {
|
|
212
|
+
// Keep enough recorder stderr/stdout for diagnostics without retaining unbounded output.
|
|
213
|
+
output = `${output}${chunk.toString()}`.slice(-16_384);
|
|
214
|
+
};
|
|
215
|
+
recordingProcess.stdout?.on('data', appendChunk);
|
|
216
|
+
recordingProcess.stderr?.on('data', appendChunk);
|
|
217
|
+
return () => output;
|
|
218
|
+
}
|