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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# cmd.exe misreads batch files with bare LF line ends (labels, long lines).
*.cmd text eol=crlf
# Shell scripts run on macOS and Linux only; a CRLF would break the shebang.
*.sh text eol=lf
375 changes: 375 additions & 0 deletions .github/workflows/campaign-windows-hermes.yml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ packages/react-native-feedback/.bugsee-feedback-sources-version
# Staged into each package by prepack (scripts/pack-native-versions.ts) and
# removed by postpack; the repo-root file is the only source.
packages/*/native-versions.json
examples/*/ios/build-spm/
# `lib/` above is for build output; these are campaign generator steps.
!scripts/campaign/lib/
6 changes: 4 additions & 2 deletions examples/bare/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ react {

/* Hermes Commands */
// Preserve the JS bundle, then inject the composed map after hermesc.
hermesCommand = file("../../node_modules/@bugsee/react-native/scripts/hermesc-preserve-js.sh").absolutePath
// Windows runs hermesCommand through cmd, which cannot run the .sh.
hermesCommand = file("../../node_modules/@bugsee/react-native/scripts/hermesc-preserve-js" +
(System.getProperty("os.name").startsWith("Windows") ? ".cmd" : ".sh")).absolutePath
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
Expand Down Expand Up @@ -149,7 +151,7 @@ android {
// createBundle<Variant>JsAndAssets the hook injects the debug id into the
// composed map and the bytecode (Hermes) or Metro's bundle and map (no Hermes),
// then uploads the map when bugsee.properties holds a real token. It fails
// the bundle task if Hermes ran without hermesc-preserve-js.sh (hermesCommand
// the bundle task if Hermes ran without the preserve wrapper (hermesCommand
// above). -PbugseeUploadSourcemaps=false turns the upload off.
apply from: file("../../node_modules/@bugsee/react-native/scripts/bugsee-sourcemaps.gradle")

Expand Down
193 changes: 178 additions & 15 deletions examples/bare/scripts/run-ios.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,137 @@
# BUGSEE_TOKEN_IOS required
# BUGSEE_ENDPOINT optional; the bridge wants the /v2 suffix, which
# App.tsx appends
# IOS_DEVICE_ID defaults to the iPhone XS Phase 1 was verified on
# IOS_DEVICE_ID defaults to the iPhone XS Phase 1 was verified on;
# only an e2e/device.ts allowlist entry is accepted
# IOS_DEVELOPMENT_TEAM required for a device build; no default, because a
# team id is per-developer
# IOS_CONFIGURATION Debug (default) or Release
#
# Campaign switches (N-27, MX-IOS-SPM), all optional:
# IOS_DELIVERY cocoapods (default) or spm. spm migrates ios/ with
# `npx react-native spm add --deintegrate --yes`
# (RN 0.87+ only), builds, asserts the embed,
# installs, then restores ios/ from git and re-runs
# pod install. IOS_SPM_KEEP=1 skips the restore.
# IOS_TARGET device (default) or simulator. simulator builds
# for iphonesimulator and installs on IOS_SIMULATOR_ID
# (default the campaign simulator 6FA9B3E8-...). The
# caller holds .device-lock (simulator) or
# .device-lock-xs (device), as for every device run.
# IOS_APP_DIR an app other than this example, e.g. a campaign
# app from scripts/campaign/gen-rn-app.sh. Its
# credentials are its own (campaign-config.json), so
# write-credentials is skipped and BUGSEE_TOKEN_IOS
# is not needed.
# IOS_LAUNCH 1: after installing, launch the app with its
# console attached for IOS_LAUNCH_SECONDS (default
# 40) and print the BUGSEE_E2E / Bugsee lines; exit
# non-zero unless IOS_LAUNCH_EXPECT (default
# "BUGSEE_E2E status=2", the contract of
# e2e/launch.test.ts) appears. The SDK banner is
# printed, never accepted: it comes before Launched.
# The device must already be held under its lock.
# Debug: the app is pointed at its own Metro with
# -RCT_jsLocation <host>:<port> (port: the app's
# campaign.port, else E2E_METRO_PORT, else 8081; host:
# localhost on the simulator, E2E_METRO_HOST or this
# Mac's en0 address on a device), Metro is started
# on that port if nothing answers there, and the
# wait defaults to 180 s.
#
# The SwiftPM delivery path is the other configuration; see README.md.
set -euo pipefail

cd "$(dirname "$0")/.."
EXAMPLE_DIR="$(pwd)"
APP_DIR="${IOS_APP_DIR:-$EXAMPLE_DIR}"
APP_DIR="$(cd "$APP_DIR" && pwd)"
DELIVERY="${IOS_DELIVERY:-cocoapods}"
TARGET="${IOS_TARGET:-device}"
case "$DELIVERY" in cocoapods|spm) ;; *) echo "IOS_DELIVERY must be cocoapods or spm" >&2; exit 2 ;; esac
case "$TARGET" in device|simulator) ;; *) echo "IOS_TARGET must be device or simulator" >&2; exit 2 ;; esac

DEVICE="${IOS_DEVICE_ID:-345BA7FE-2C29-5722-892A-BFCB1FD34D0C}"
: "${IOS_DEVELOPMENT_TEAM:?set IOS_DEVELOPMENT_TEAM to your Apple Developer team id}"
SIMULATOR="${IOS_SIMULATOR_ID:-6FA9B3E8-26C7-4232-AA2C-537D9DF32957}"
if [[ "$TARGET" == device ]]; then
: "${IOS_DEVELOPMENT_TEAM:?set IOS_DEVELOPMENT_TEAM to your Apple Developer team id}"
# The same fail-closed allowlist the e2e uses: never another iPhone.
node -e '
const id = process.argv[1].trim().toUpperCase();
const src = require("fs").readFileSync(process.argv[2], "utf8");
const allow = src.slice(src.indexOf("IOS_DEVICE_ALLOWLIST"), src.indexOf("IOS_DEVICE_REFUSED"));
if (/^D027034D/i.test(id) || !allow.includes(`\x27${id}\x27`)) {
console.error(`IOS_DEVICE_ID=${id} is not on the e2e/device.ts allowlist`);
process.exit(1);
}' "$DEVICE" "$EXAMPLE_DIR/e2e/device.ts"
fi
# Debug by default; Release for the gated E2E_RELEASE=1 device cases (Task 7.5b).
CONFIGURATION="${IOS_CONFIGURATION:-Debug}"
APP="ios/build/Build/Products/${CONFIGURATION}-iphoneos/BareExample.app"
SCHEME="$(basename "$(ls -d "$APP_DIR"/ios/*.xcodeproj | head -1)" .xcodeproj)"
SDK=iphoneos
[[ "$TARGET" == simulator ]] && SDK=iphonesimulator
BUILD_DIR="ios/build"
[[ "$DELIVERY" == spm ]] && BUILD_DIR="ios/build-spm"
APP="$APP_DIR/$BUILD_DIR/Build/Products/${CONFIGURATION}-${SDK}/${SCHEME}.app"
Comment thread
cursor[bot] marked this conversation as resolved.

if [[ "$APP_DIR" == "$EXAMPLE_DIR" ]]; then
node scripts/write-credentials.mjs
fi

restore_cocoapods() {
local status=$?
set +e
echo "--- restoring CocoaPods delivery in $APP_DIR/ios"
if git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$APP_DIR" checkout -- ios
git -C "$APP_DIR" clean -fdq -- ios
else
rm -rf "$APP_DIR/ios"
tar -xf "$SPM_BACKUP" -C "$APP_DIR"
fi
(cd "$APP_DIR/ios" && pod install >/dev/null) || { echo "restore: pod install failed"; exit 1; }
# The SPM build leaves xcshareddata/swiftpm/Package.resolved behind.
if git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git -C "$APP_DIR" clean -fdq -- ios
fi
echo "--- restored"
exit "$status"
}

node scripts/write-credentials.mjs
if [[ "$DELIVERY" == spm ]]; then
if git -C "$APP_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
# The restore is a git checkout of ios/, so refuse to start over edits
# it would throw away.
git -C "$APP_DIR" diff --quiet -- ios \
|| { echo "ios/ has uncommitted changes; commit or stash them before IOS_DELIVERY=spm" >&2; exit 1; }
else
# A campaign app outside git: keep a copy of ios/ (without Pods/build).
SPM_BACKUP="$(mktemp -t ios-cocoapods).tar"
tar -cf "$SPM_BACKUP" -C "$APP_DIR" --exclude ios/Pods --exclude 'ios/build*' ios
fi
[[ "${IOS_SPM_KEEP:-0}" == 1 ]] || trap restore_cocoapods EXIT
# --deintegrate leaves Podfile broken (README.md), hence the restore above.
(cd "$APP_DIR" && npx react-native spm add --deintegrate --yes)
else
(cd "$APP_DIR/ios" && pod install)
fi

(cd ios && pod install)
PROJECT_ARGS=(-workspace "$APP_DIR/ios/$SCHEME.xcworkspace")
[[ -d "$APP_DIR/ios/$SCHEME.xcworkspace" ]] || PROJECT_ARGS=(-project "$APP_DIR/ios/$SCHEME.xcodeproj")
DESTINATION="id=$DEVICE"
SIGNING=(DEVELOPMENT_TEAM="${IOS_DEVELOPMENT_TEAM:-}" CODE_SIGN_STYLE=Automatic -allowProvisioningUpdates)
if [[ "$TARGET" == simulator ]]; then
DESTINATION="id=$SIMULATOR"
SIGNING=()
fi

xcodebuild \
-workspace ios/BareExample.xcworkspace \
-scheme BareExample \
"${PROJECT_ARGS[@]}" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination "id=$DEVICE" \
-derivedDataPath ios/build \
DEVELOPMENT_TEAM="$IOS_DEVELOPMENT_TEAM" \
CODE_SIGN_STYLE=Automatic \
-allowProvisioningUpdates \
-destination "$DESTINATION" \
-derivedDataPath "$APP_DIR/$BUILD_DIR" \
${SIGNING[@]+"${SIGNING[@]}"} \
build

# Before installing, not after: an app that builds without the framework is the
Expand All @@ -55,7 +158,67 @@ node -e 'const [maj,min]=process.versions.node.split(".").map(Number);
}'
node ../../scripts/cli-assert-framework-embedded.ts "$APP"

xcrun devicectl device install app --device "$DEVICE" "$APP"
if [[ "$TARGET" == simulator ]]; then
xcrun simctl install "$SIMULATOR" "$APP"
echo
echo "Installed on simulator $SIMULATOR ($DELIVERY). Now: E2E_PLATFORM=ios E2E_IOS_TARGET=simulator yarn e2e"
else
xcrun devicectl device install app --device "$DEVICE" "$APP"
echo
echo "Installed on $DEVICE ($DELIVERY). Now: E2E_PLATFORM=ios E2E_IOS_TARGET=device yarn e2e"
fi

echo
echo "Installed on $DEVICE. Now: E2E_PLATFORM=ios E2E_IOS_TARGET=device yarn e2e"
if [[ "${IOS_LAUNCH:-0}" == 1 ]]; then
BUNDLE_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Info.plist")"
CONSOLE="$(mktemp -t run-ios-console)"
APP_ARGS=()
LAUNCH_SECONDS="${IOS_LAUNCH_SECONDS:-40}"
if [[ "$CONFIGURATION" == Debug ]]; then
METRO_PORT="$(cat "$APP_DIR/campaign.port" 2>/dev/null || echo "${E2E_METRO_PORT:-8081}")"
METRO_HOST=localhost
[[ "$TARGET" == device ]] && METRO_HOST="${E2E_METRO_HOST:-$(ipconfig getifaddr en0)}"
if ! curl -sf "http://localhost:$METRO_PORT/status" | grep -q running; then
if grep -q '"expo"' "$APP_DIR/app.json" 2>/dev/null; then
(cd "$APP_DIR" && CI=1 exec npx expo start --port "$METRO_PORT") >"$CONSOLE.metro" 2>&1 </dev/null &
else
(cd "$APP_DIR" && exec npx react-native start --port "$METRO_PORT") >"$CONSOLE.metro" 2>&1 </dev/null &
fi
METRO_PID=$!
for _ in $(seq 1 90); do
curl -sf "http://localhost:$METRO_PORT/status" | grep -q running && break
sleep 1
done
fi
APP_ARGS=(-RCT_jsLocation "$METRO_HOST:$METRO_PORT")
LAUNCH_SECONDS="${IOS_LAUNCH_SECONDS:-180}"
fi
if [[ "$TARGET" == simulator ]]; then
xcrun simctl launch --console-pty --terminate-running-process "$SIMULATOR" "$BUNDLE_ID" \
${APP_ARGS[@]+"${APP_ARGS[@]}"} >"$CONSOLE" 2>&1 </dev/null &
else
# `--` ends devicectl's own options, so the app's arguments reach the app.
xcrun devicectl device process launch --device "$DEVICE" --terminate-existing --console "$BUNDLE_ID" \
-- ${APP_ARGS[@]+"${APP_ARGS[@]}"} >"$CONSOLE" 2>&1 </dev/null &
fi
LAUNCH_PID=$!
for _ in $(seq 1 "$LAUNCH_SECONDS"); do
grep -qE "${IOS_LAUNCH_EXPECT:-BUGSEE_E2E status=2}" "$CONSOLE" && break
sleep 1
done
sleep 2
if [[ -n "${METRO_PID:-}" ]]; then
pkill -P "$METRO_PID" 2>/dev/null || true
kill "$METRO_PID" 2>/dev/null || true
lsof -t -iTCP:"$METRO_PORT" -sTCP:LISTEN 2>/dev/null | xargs kill 2>/dev/null || true
fi
kill "$LAUNCH_PID" 2>/dev/null || true
wait "$LAUNCH_PID" 2>/dev/null || true
echo "--- console of $BUNDLE_ID ($CONFIGURATION, $DELIVERY, $TARGET)"
grep -E "BUGSEE_E2E|Bugsee|BugseeRN" "$CONSOLE" | cut -c1-240 | head -40
if grep -qE "${IOS_LAUNCH_EXPECT:-BUGSEE_E2E status=2}" "$CONSOLE"; then
echo "launch: PASS"
else
echo "launch: FAIL (no match for ${IOS_LAUNCH_EXPECT:-BUGSEE_E2E status=2}; console kept at $CONSOLE)"
exit 1
fi
fi
103 changes: 101 additions & 2 deletions examples/expo/App.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,107 @@
/**
* The Expo example (N-20): launches Bugsee and prints the markers the device
* e2e and scripts/campaign/launch-check.sh read, as examples/bare does:
*
* BUGSEE_E2E launching on <os> rn=<x.y.z> engine=<hermes|jsc> scenario=<name>
* BUGSEE_E2E status=<n> (<name>)
*
* Credentials: campaign-config.json beside this file, which holds only the
* placeholder token and the dead endpoint. Never a real token.
*
* The scenario comes from the e2e deep link
* `bugsee-e2e://scenario/<name>?nonce=<hex>` (app.json `scheme`), the channel
* examples/bare uses on Android. `launch` is the default. The N-01 smoke
* scenarios register in SCENARIOS below (additive edits only).
*/
/* global setInterval, clearInterval */
const React = require('react');
const { Text, View } = require('react-native');
const { Linking, Platform, Text, View } = require('react-native');
const Bugsee = require('@bugsee/react-native');

const { default: bugsee, BugseeLaunchOptions, Status, createDefaultLaunchOptions } = Bugsee;

const config = require('./campaign-config.json');

const STATUS_NAMES = {
[Status.Stopped]: 'Stopped',
[Status.Launching]: 'Launching',
[Status.Launched]: 'Launched',
[Status.Stopping]: 'Stopping',
};

/** Scenario name -> async (nonce) => void, run once Launched. */
const SCENARIOS = {
launch: async () => undefined,
};

const SCENARIO_URI = /^bugsee-e2e:\/\/scenario\/([\w-]+)\?nonce=([0-9a-f]+)/;

async function chooseScenario() {
const uri = await Linking.getInitialURL().catch(() => null);
const match = uri == null ? null : SCENARIO_URI.exec(uri);
return match ? { scenario: match[1], nonce: match[2] } : { scenario: 'launch', nonce: '-' };
}

function engine() {
return globalThis.HermesInternal ? 'hermes' : 'jsc';
}

function App() {
return React.createElement(View, null, React.createElement(Text, null, 'Bugsee Expo'));
const [status, setStatus] = React.useState(Status.Stopped);

React.useEffect(() => {
let last = -1;
let cancelled = false;
const poll = setInterval(() => {
bugsee
.getStatus()
.then((next) => {
if (!cancelled && next !== last) {
last = next;
setStatus(next);
console.log(`BUGSEE_E2E status=${next} (${STATUS_NAMES[next]})`);
}
})
.catch(() => undefined);
}, 100);

(async () => {
const { scenario, nonce } = await chooseScenario();
const rn = Platform.constants.reactNativeVersion;
console.log(
`BUGSEE_E2E launching on ${Platform.OS} rn=${rn.major}.${rn.minor}.${rn.patch} ` +
`engine=${engine()} scenario=${scenario}`,
);
const options = createDefaultLaunchOptions();
options.endpoint = config.endpoint;
const token = Platform.OS === 'ios' ? config.ios : config.android;
try {
const ok = await bugsee.launch(token, BugseeLaunchOptions.serialize(options));
console.log(`BUGSEE_E2E launch() resolved ${ok}`);
} catch (e) {
console.log(`BUGSEE_E2E launch() rejected ${e instanceof Error ? e.message : String(e)}`);
return;
}
const run = SCENARIOS[scenario];
if (run === undefined) {
console.log(`BUGSEE_E2E unknown scenario ${scenario}`);
return;
}
await run(nonce);
console.log(`BUGSEE_E2E scenario ${scenario} done nonce=${nonce}`);
})();

return () => {
cancelled = true;
clearInterval(poll);
};
}, []);

return React.createElement(
View,
{ style: { flex: 1, alignItems: 'center', justifyContent: 'center' } },
React.createElement(Text, null, `Bugsee Expo: ${STATUS_NAMES[status]}`),
);
}

module.exports = App;
3 changes: 2 additions & 1 deletion examples/expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"plugins": [
"@bugsee/react-native"
]
],
"scheme": "bugsee-e2e"
}
}
5 changes: 5 additions & 0 deletions examples/expo/campaign-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ios": "00000000-0000-4000-8000-000000000000",
"android": "00000000-0000-4000-8000-000000000000",
"endpoint": "https://127.0.0.1:9"
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"assert:ios-embed": "node scripts/cli-assert-framework-embedded.ts",
"mutate:src": "stryker run stryker.src.json",
"mutate:scripts": "stryker run stryker.scripts.json",
"mutate:plugin": "stryker run stryker.plugin.json; status=$?; chmod 755 packages/react-native/scripts/compose-then-inject.js; exit $status"
"mutate:plugin": "stryker run stryker.plugin.json; status=$?; chmod 755 packages/react-native/scripts/compose-then-inject.js packages/react-native/scripts/hermesc-preserve-js.js; exit $status"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
Loading
Loading