Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/scripts/boot-simulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ wait_for_simulator_ready() {
return 1
}

BOOT_MODE="${RNFB_SIM_BOOT_MODE:-full}"

# shellcheck source=simulator-logging.sh
source "$(dirname "$0")/simulator-logging.sh"

if [[ "$BOOT_MODE" == "logs" ]]; then
restart_simulator_logging || true
exit 0
fi

# Get our simulator name from our test Detox config
pushd "$(dirname "$0")/../../../tests" >/dev/null || exit 1
SIM="$(grep iPhone .detoxrc.js | head -1 | cut -d"'" -f2)"
Expand Down Expand Up @@ -220,3 +230,7 @@ log_boot_status "install complete in $((SECONDS - install_start))s"
popd >/dev/null || exit 1

log_boot_status "phase=complete device=\"${SIM}\" ready with test app installed"

if [[ "${RNFB_START_SIM_LOGS:-1}" == "1" && "$BOOT_MODE" == "full" ]]; then
restart_simulator_logging || true
fi
9 changes: 9 additions & 0 deletions .github/workflows/scripts/deploy-e2e-cloud-functions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
# Deploy live-project e2e helper callables (Cloud Logging metrics, App Check, RC admin).
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

yarn firebase -c "$(pwd)/firebase.json" --project react-native-firebase-testing deploy --only \
functions:e2eCloudMetricsV2,functions:e2eCloudMetricsSummaryV2,functions:fetchAppCheckTokenV2,functions:testFunctionRemoteConfigUpdateV2
2 changes: 0 additions & 2 deletions .github/workflows/scripts/deploy-remote-config-function.sh

This file was deleted.

24 changes: 20 additions & 4 deletions .github/workflows/scripts/flake-summary.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,34 @@ DETOX_LOG="${RNFB_DETOX_LOG:-}"
fi
echo "=== ${label}: ${file} ($(wc -l <"$file" | tr -d ' ') lines) ==="
rg -n \
'\[jet-ws\]|\[rnfb-e2e\]|\[jet-coverage\]|\[rnfb-lifecycle\]|RETRYABLE_DISCONNECT|reconnect_recovered|FrontBoard|FBSOpenApplication|coverage-ready|retry-eligibility|install-state|terminateApp|launchApp failure|Jet attempt|FAIL e2e' \
'\[jet-ws\]|\[rnfb-e2e\]|\[jet-coverage\]|\[rnfb-lifecycle\]|RETRYABLE_DISCONNECT|reconnect_recovered|FrontBoard|FBSOpenApplication|coverage-ready|retry-eligibility|install-state|terminateApp|launchApp failure|Jet attempt|FAIL e2e|orchestrate-state|Child process terminated|log stream restarted|\[load-settle\]' \
"$file" 2>/dev/null | tail -200 || true
echo ""
}

summarize "detox-log" "${DETOX_LOG}"
summarize "simulator-log" "simulator.log"
summarize "testing-log" "testing.log"
summarize "springboard-log" "springboard-invertase.log"
summarize "sim-app-log" "sim-app.log"
summarize "resource-monitor" "resource-monitor.log"
summarize "resource-monitor-android" "resource-monitor-android.log"
summarize "metro-log" "metro.log"

echo "=== disconnect_context vs resource-monitor load ==="
if [[ -f "${DETOX_LOG}" ]]; then
rg -n '\[jet-ws\] disconnect_context' "${DETOX_LOG}" 2>/dev/null | tail -20 || true
fi
if [[ -f "resource-monitor.log" ]]; then
echo "--- resource-monitor load around disconnect (last 80 load lines) ---"
rg -n 'load averages|^\[resource-monitor\] ps-empty' resource-monitor.log 2>/dev/null | tail -80 || true
fi
echo ""

echo "=== log stream death markers ==="
for f in sim-app.log; do
if [[ -f "$f" ]]; then
rg -n 'Child process terminated|log stream restarted' "$f" 2>/dev/null | tail -20 || true
fi
done
echo ""
} >"$OUT"

echo "[flake-summary] wrote ${OUT} ($(wc -l <"$OUT" | tr -d ' ') lines)"
1 change: 1 addition & 0 deletions .github/workflows/scripts/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"main": "lib/index.js",
"dependencies": {
"@google-cloud/logging": "^11.2.1",
"firebase-admin": "^13.8.0",
"firebase-functions": "^7.2.5"
},
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/scripts/functions/src/e2eCallOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Server timeout for e2e callables that are not probing client deadline behavior. */
export const E2E_TEST_FUNCTION_TIMEOUT_SECONDS = 120;
153 changes: 153 additions & 0 deletions .github/workflows/scripts/functions/src/e2eCloudMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Logging, type Entry } from '@google-cloud/logging';
import { logger } from 'firebase-functions/v2';
import { CallableRequest, onCall } from 'firebase-functions/v2/https';

export type E2eCloudMetricPayload = {
source?: string;
category: string;
event: string;
platform?: string;
durationMs?: number;
attempt?: number;
error?: string;
metadata?: Record<string, unknown>;
};

const METRICS_LOG_MESSAGE = '[rnfb-e2e-metrics]';
const SUMMARY_QUERY_EVENT = 'summary-query';

function logE2eCloudMetric(payload: Record<string, unknown>): void {
logger.write({
severity: 'INFO',
message: METRICS_LOG_MESSAGE,
...payload,
});
}

function parseJsonObject(value: unknown): Record<string, unknown> | null {
if (!value) {
return null;
}
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
} catch {
return null;
}
}
return null;
}

function metricPayloadFromLogEntry(entry: Entry): Record<string, unknown> | null {
const metadata = entry.metadata as Record<string, unknown> | undefined;
const candidates = [
metadata?.jsonPayload,
entry.data,
metadata?.textPayload,
];

for (const candidate of candidates) {
const parsed = parseJsonObject(candidate);
if (!parsed || parsed.message !== METRICS_LOG_MESSAGE) {
continue;
}
if (parsed.event === SUMMARY_QUERY_EVENT) {
return null;
}
return parsed;
}

return null;
}

/**
* Records e2e cloud-pressure events in Cloud Logging (live project only).
* Grep Cloud Logging for `[rnfb-e2e-metrics]` to aggregate cross-run pressure.
*/
export const e2eCloudMetricsV2 = onCall(
{ timeoutSeconds: 30 },
async (req: CallableRequest<E2eCloudMetricPayload>) => {
const payload = {
...req.data,
receivedAt: new Date().toISOString(),
};
logE2eCloudMetric(payload);
return { ok: true };
},
);

type MetricsSummary = {
lookbackHours: number;
since: string;
total: number;
byCategory: Record<string, number>;
byEvent: Record<string, number>;
byPlatform: Record<string, number>;
};

/**
* Summarizes recent `[rnfb-e2e-metrics]` entries from Cloud Logging.
* Callable for retrospective cross-run pressure analysis (not logged per Detox run).
*/
export const e2eCloudMetricsSummaryV2 = onCall(
{ timeoutSeconds: 60 },
async (req: CallableRequest<{ lookbackHours?: number }>) => {
const lookbackHours = req.data?.lookbackHours ?? 24;
const since = new Date(Date.now() - lookbackHours * 60 * 60 * 1000);

const logging = new Logging();
const filter = [
'resource.type="cloud_run_revision"',
`timestamp>="${since.toISOString()}"`,
`jsonPayload.message="${METRICS_LOG_MESSAGE}"`,
].join(' AND ');

const summary: MetricsSummary = {
lookbackHours,
since: since.toISOString(),
total: 0,
byCategory: {},
byEvent: {},
byPlatform: {},
};

let pageToken: string | undefined;
let scanned = 0;
const maxEntries = 5000;

do {
const [entries, , response] = await logging.getEntries({
filter,
pageSize: 500,
pageToken,
});

for (const entry of entries) {
const payload = metricPayloadFromLogEntry(entry);
if (!payload) {
continue;
}

summary.total++;
const category = String(payload.category || 'unknown');
const event = String(payload.event || 'unknown');
const platform = String(payload.platform || payload.source || 'unknown');
summary.byCategory[category] = (summary.byCategory[category] || 0) + 1;
summary.byEvent[event] = (summary.byEvent[event] || 0) + 1;
summary.byPlatform[platform] = (summary.byPlatform[platform] || 0) + 1;
}

scanned += entries.length;
pageToken = response?.nextPageToken || undefined;
} while (pageToken && scanned < maxEntries);

logE2eCloudMetric({ event: SUMMARY_QUERY_EVENT, ...summary });
return summary;
},
);
18 changes: 11 additions & 7 deletions .github/workflows/scripts/functions/src/fetchAppCheckToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@

import { getAppCheck } from 'firebase-admin/app-check';
import { CallableRequest, onCall } from 'firebase-functions/v2/https';
import { E2E_TEST_FUNCTION_TIMEOUT_SECONDS } from './e2eCallOptions';
import { getAdminApp } from '.';

// Note: this will only work in a live environment, not locally via the Firebase emulator.
export const fetchAppCheckTokenV2 = onCall(async (req: CallableRequest<{ appId: string }>) => {
const { appId } = req.data;
const expireTimeMillis = Math.floor(Date.now() / 1000) + 60 * 60;
getAdminApp();
const result = await getAppCheck().createToken(appId);
return { ...result, expireTimeMillis };
});
export const fetchAppCheckTokenV2 = onCall(
{ timeoutSeconds: E2E_TEST_FUNCTION_TIMEOUT_SECONDS },
async (req: CallableRequest<{ appId: string }>) => {
const { appId } = req.data;
const expireTimeMillis = Math.floor(Date.now() / 1000) + 60 * 60;
getAdminApp();
const result = await getAppCheck().createToken(appId);
return { ...result, expireTimeMillis };
},
);
2 changes: 2 additions & 0 deletions .github/workflows/scripts/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export { sendFCM } from './sendFCM';

export { testFetchStream, testFetch } from './vertexaiFunctions';

export { e2eCloudMetricsV2, e2eCloudMetricsSummaryV2 } from './e2eCloudMetrics';

export {
testStreamingCallable,
testProgressStream,
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/scripts/functions/src/sendFCM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
import { logger } from 'firebase-functions/v2';
import { CallableRequest, onCall } from 'firebase-functions/v2/https';
import { getMessaging, TokenMessage } from 'firebase-admin/messaging';
import { E2E_TEST_FUNCTION_TIMEOUT_SECONDS } from './e2eCallOptions';
import { getAdminApp } from '.';

// Note: this will only work in a live environment, not locally via the Firebase emulator.
export const sendFCM = onCall(
{ timeoutSeconds: E2E_TEST_FUNCTION_TIMEOUT_SECONDS },
async (req: CallableRequest<{ message: TokenMessage; delay?: number }>) => {
const { message, delay } = req.data;
return await new Promise(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
*/

import { onCall } from 'firebase-functions/v2/https';
import { E2E_TEST_FUNCTION_TIMEOUT_SECONDS } from './e2eCallOptions';

export const testFunctionCustomRegion = onCall(
{
region: 'europe-west1',
timeoutSeconds: E2E_TEST_FUNCTION_TIMEOUT_SECONDS,
},
() => 'europe-west1',
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import { deepEqual } from 'assert';
import { FirebaseError } from 'firebase-admin';
import { CallableRequest, onCall, HttpsError } from 'firebase-functions/v2/https';
import { E2E_TEST_FUNCTION_TIMEOUT_SECONDS } from './e2eCallOptions';
import SAMPLE_DATA from './sample-data';

export const testFunctionDefaultRegionV2 = onCall(
{ timeoutSeconds: E2E_TEST_FUNCTION_TIMEOUT_SECONDS },
(req: CallableRequest<{ type: string; asError: boolean; inputData: any }>) => {
console.log(Date.now(), req.data);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import { getRemoteConfig } from 'firebase-admin/remote-config';
import { logger } from 'firebase-functions/v2';
import { CallableRequest, onCall } from 'firebase-functions/v2/https';
import { E2E_TEST_FUNCTION_TIMEOUT_SECONDS } from './e2eCallOptions';
import { getAdminApp } from '.';

export const testFunctionRemoteConfigUpdateV2 = onCall(
{
maxInstances: 1,
concurrency: 1,
timeoutSeconds: E2E_TEST_FUNCTION_TIMEOUT_SECONDS,
},
async (
req: CallableRequest<{
Expand Down
Loading
Loading