feat(ota): wysylaj source mapy paczek OTA do Sentry
Bez tego mapy mial WYLACZNIE bundle wbudowany w APK, a praktycznie kazdy uzytkownik dociaga paczke OTA przy pierwszym uruchomieniu i od tego momentu w Sentry widac `app:///index.android.bundle` zamiast nazw plikow i numerow linii. Czyli przebudowa APK z tokenem sama w sobie dawala czytelne slady na kilkadziesiat sekund zycia instalacji. Trzy zmiany w `publish_update.py`: * `expo export` z `--source-maps` * `upload_sourcemaps()` woła oficjalny `expo-upload-sourcemaps.js` z @sentry/react-native. Dopasowanie idzie po `debug id` zaszytym w paczce, wiec nazwa release'u ani dist nie sa potrzebne. Brak `SENTRY_AUTH_TOKEN` NIE jest bledem, bo publikacja ma dzialac takze u kogos, kto stawia wlasna instancje; wtedy tylko o tym mowimy. Nieudana wysylka rowniez nie blokuje publikacji: gorsza jest apka bez poprawki niz nieczytelny slad. * `strip_sourcemaps()` kasuje mapy z `dist` PRZED scp. Mapa wazy 11 MB przy 3 MB paczki i zawiera caly kod zrodlowy, a `scp -r .` wyslalby ja razem z reszta, wiec lezalaby publicznie obok paczki. `metadata.json` jej nie wymienia, wiec manifest sie nie psuje (sprawdzone). Pulapka skryptu Sentry: siega po konfiguracje Expo, gdy brakuje KTOREGOKOLWIEK z SENTRY_ORG, SENTRY_PROJECT i SENTRY_URL, i wtedy konczy bledem. SENTRY_URL nie jest sekretem, wiec podstawiamy sentry.io. Musi tez dzialac z katalogu `mobile`, bo zaklada, ze stoi w korzeniu projektu. Publikuj przez `iclaude goon -- ...`, wtedy token wjezdza ze srodowiska. Zweryfikowane end-to-end na katalogu testowym na VPS, bez ruszania zywych kanalow: mapy poszly do Sentry (debug id e2feb975), zostaly usuniete z dist (11,1 MB), manifest zbudowany z 23 assetami, a na serwerze wyladowalo 0 plikow .map. Przy okazji ustalone: paczki juz stojace na wszystkich trzech kanalach maja identyczna tresc z ta, ktorej mapy wyslalem, wiec slady dla obecnych uzytkownikow odczytaja sie bez zmuszania kogokolwiek do ponownego pobrania 3 MB.
This commit is contained in:
parent
031c309091
commit
2c92fab2a9
1 changed files with 68 additions and 4 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
"""Publish a new Expo Updates JS bundle do VPS-a (silent OTA update).
|
"""Publish a new Expo Updates JS bundle do VPS-a (silent OTA update).
|
||||||
|
|
||||||
Workflow:
|
Workflow:
|
||||||
1. `npx expo export --platform android` -> produkuje `mobile/dist/`
|
1. `npx expo export --platform android --source-maps` -> produkuje `mobile/dist/`
|
||||||
|
1b. Mapy lecą do Sentry (`upload_sourcemaps`), a potem znikają z `dist`
|
||||||
|
(`strip_sourcemaps`), żeby nie pojechały na serwer razem z paczką
|
||||||
2. Czyta `dist/metadata.json` (file->hash mapping wygenerowany przez Expo)
|
2. Czyta `dist/metadata.json` (file->hash mapping wygenerowany przez Expo)
|
||||||
3. Buduje manifest JSON wg Expo Updates v1 protocol
|
3. Buduje manifest JSON wg Expo Updates v1 protocol
|
||||||
4. scp dist/ + manifest.json -> `/root/goon/app/static/expo-updates/<runtime>/<update_id>/`
|
4. scp dist/ + manifest.json -> `/root/goon/app/static/expo-updates/<runtime>/<update_id>/`
|
||||||
|
|
@ -63,7 +65,7 @@ def sha256_b64url(data: bytes) -> str:
|
||||||
return base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()
|
return base64.urlsafe_b64encode(hashlib.sha256(data).digest()).rstrip(b"=").decode()
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: list[str], cwd: Path | None = None) -> None:
|
def run(cmd: list[str], cwd: Path | None = None, extra_env: dict | None = None) -> None:
|
||||||
print(f"$ {' '.join(cmd)}")
|
print(f"$ {' '.join(cmd)}")
|
||||||
# Windows: `npx` to `.cmd` więc subprocess wymaga `shell=True` (CreateProcess
|
# Windows: `npx` to `.cmd` więc subprocess wymaga `shell=True` (CreateProcess
|
||||||
# nie wie jak rozwiązać .cmd extension bez shella). Linux/Mac: shell=False
|
# nie wie jak rozwiązać .cmd extension bez shella). Linux/Mac: shell=False
|
||||||
|
|
@ -71,13 +73,70 @@ def run(cmd: list[str], cwd: Path | None = None) -> None:
|
||||||
# więc i tak nie ma shell injection risk.
|
# więc i tak nie ma shell injection risk.
|
||||||
shell = sys.platform == "win32" and cmd[0] in ("npx", "npm", "scp", "ssh")
|
shell = sys.platform == "win32" and cmd[0] in ("npx", "npm", "scp", "ssh")
|
||||||
# MSYS_NO_PATHCONV=1, blokuje konwersję `/root/...` argów ssh/scp przez git-bash sh.
|
# MSYS_NO_PATHCONV=1, blokuje konwersję `/root/...` argów ssh/scp przez git-bash sh.
|
||||||
env = {**os.environ, "MSYS_NO_PATHCONV": "1"}
|
env = {**os.environ, "MSYS_NO_PATHCONV": "1", **(extra_env or {})}
|
||||||
if shell:
|
if shell:
|
||||||
subprocess.run(" ".join(cmd), cwd=cwd, check=True, shell=True, env=env)
|
subprocess.run(" ".join(cmd), cwd=cwd, check=True, shell=True, env=env)
|
||||||
else:
|
else:
|
||||||
subprocess.run(cmd, cwd=cwd, check=True, env=env)
|
subprocess.run(cmd, cwd=cwd, check=True, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
SENTRY_UPLOADER = (
|
||||||
|
MOBILE / "node_modules" / "@sentry" / "react-native" / "scripts" / "expo-upload-sourcemaps.js"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upload_sourcemaps() -> None:
|
||||||
|
"""Wysyła mapy tej paczki do Sentry, żeby ślady JS dało się odczytać.
|
||||||
|
|
||||||
|
Po co: mapy z buildu APK opisują WYŁĄCZNIE paczkę wbudowaną w plik instalacyjny, a
|
||||||
|
praktycznie każdy użytkownik dociąga OTA przy pierwszym uruchomieniu i od tego
|
||||||
|
momentu w Sentry widać `app:///index.android.bundle` zamiast nazw plików i numerów
|
||||||
|
linii. Dopasowanie idzie po `debug id` zaszytym w paczce, więc nazwa release'u ani
|
||||||
|
dist nie są do niczego potrzebne.
|
||||||
|
|
||||||
|
Brak `SENTRY_AUTH_TOKEN` NIE jest błędem. Publikacja ma działać także u kogoś, kto
|
||||||
|
stawia własną instancję i nie ma naszego Sentry; wtedy tylko o tym mówimy. U nas
|
||||||
|
token wstrzykuje wrapper `iclaude` (workspace `goon-app`), więc publikuj przez niego.
|
||||||
|
"""
|
||||||
|
if not os.environ.get("SENTRY_AUTH_TOKEN"):
|
||||||
|
print("Sentry: brak SENTRY_AUTH_TOKEN, pomijam wysyłkę map (slady JS beda nieczytelne)")
|
||||||
|
return
|
||||||
|
if not SENTRY_UPLOADER.exists():
|
||||||
|
print(f"Sentry: nie znalazlem {SENTRY_UPLOADER}, pomijam wysyłkę map")
|
||||||
|
return
|
||||||
|
# Skrypt Sentry sięga po konfigurację Expo, gdy brakuje KTÓREGOKOLWIEK z org, project
|
||||||
|
# i url, i wtedy kończy błędem. Dla sentry.io ten adres jest stały, więc podstawiamy
|
||||||
|
# domyślny i zostajemy przy zmiennych środowiskowych.
|
||||||
|
# Musi też działać z katalogu `mobile`, bo zakłada, że stoi w korzeniu projektu.
|
||||||
|
try:
|
||||||
|
run(
|
||||||
|
["node", str(SENTRY_UPLOADER), str(DIST)],
|
||||||
|
cwd=MOBILE,
|
||||||
|
extra_env={"SENTRY_URL": os.environ.get("SENTRY_URL") or "https://sentry.io"},
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# Nieudana wysyłka map NIE MOŻE zablokować publikacji: gorsza jest apka bez
|
||||||
|
# poprawki niż nieczytelny ślad w Sentry.
|
||||||
|
print(f"Sentry: wysyłka map nie powiodła się ({e}), publikuję mimo to")
|
||||||
|
|
||||||
|
|
||||||
|
def strip_sourcemaps() -> None:
|
||||||
|
"""Kasuje mapy z `dist`, zanim cokolwiek pojedzie na serwer.
|
||||||
|
|
||||||
|
Mapa waży około 10,7 MB przy 3 MB paczki i zawiera cały kod źródłowy. `metadata.json`
|
||||||
|
jej nie wymienia, więc manifest bez niej działa, ale `scp -r .` wysłałby ją razem z
|
||||||
|
resztą i leżałaby publicznie obok paczki.
|
||||||
|
"""
|
||||||
|
removed = 0
|
||||||
|
freed = 0
|
||||||
|
for path in DIST.rglob("*.map"):
|
||||||
|
freed += path.stat().st_size
|
||||||
|
path.unlink()
|
||||||
|
removed += 1
|
||||||
|
if removed:
|
||||||
|
print(f"Usunięto {removed} map(y) z dist ({freed / 1e6:.1f} MB), nie ida na serwer")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--runtime", default=RUNTIME_DEFAULT)
|
ap.add_argument("--runtime", default=RUNTIME_DEFAULT)
|
||||||
|
|
@ -88,7 +147,12 @@ def main() -> int:
|
||||||
if DIST.exists():
|
if DIST.exists():
|
||||||
import shutil
|
import shutil
|
||||||
shutil.rmtree(DIST)
|
shutil.rmtree(DIST)
|
||||||
run(["npx", "expo", "export", "--platform", "android"], cwd=MOBILE)
|
# `--source-maps` dokłada `.hbc.map`, potrzebną do odczytania śladów JS
|
||||||
|
# w Sentry. Mapa NIE jedzie na serwer, kasuje ją `strip_sourcemaps()`.
|
||||||
|
run(["npx", "expo", "export", "--platform", "android", "--source-maps"], cwd=MOBILE)
|
||||||
|
|
||||||
|
upload_sourcemaps()
|
||||||
|
strip_sourcemaps()
|
||||||
|
|
||||||
metadata_path = DIST / "metadata.json"
|
metadata_path = DIST / "metadata.json"
|
||||||
if not metadata_path.exists():
|
if not metadata_path.exists():
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue