From feb4541df68bc5884d4704d2629e4b315982eb51 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 13 Aug 2026 16:59:48 +0200 Subject: [PATCH 01/18] capture all the envelopes --- .github/workflows/ci.yml | 61 ++++- .github/workflows/test-run-android.yml | 22 ++ .github/workflows/test-run-desktop.yml | 19 ++ .github/workflows/test-run-ios.yml | 20 ++ .github/workflows/test-run-webgl.yml | 20 ++ scripts/replay-envelopes.py | 237 ++++++++++++++++++ test/IntegrationTest/Integration.Tests.ps1 | 53 +++- .../Editor/AllowInsecureHttp.cs | 62 +++++ .../envelope-capture-server.py | 222 ++++++++++++++++ .../start-capture-server.ps1 | 42 ++++ 10 files changed, 737 insertions(+), 21 deletions(-) create mode 100644 scripts/replay-envelopes.py create mode 100644 test/Scripts.Integration.Test/envelope-capture-server.py create mode 100644 test/Scripts.Integration.Test/start-capture-server.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94ca1885c..bcc52bb5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -238,7 +238,7 @@ jobs: run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: BUILD_PLATFORM: ${{ matrix.build_platform }} - SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md - name: Build Project run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" @@ -294,7 +294,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} @@ -308,7 +308,7 @@ jobs: needs: [test-build-android, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md uses: ./.github/workflows/test-run-android.yml with: unity-version: ${{ matrix.unity-version }} @@ -329,7 +329,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} @@ -362,7 +362,7 @@ jobs: init-type: ${{ matrix.init-type }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -386,7 +386,7 @@ jobs: needs: [test-build-webgl, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -402,7 +402,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -418,7 +418,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -434,7 +434,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -449,7 +449,7 @@ jobs: needs: [test-build-linux, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -467,7 +467,7 @@ jobs: needs: [test-build-windows, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -485,7 +485,7 @@ jobs: needs: [test-build-macos, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} + SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -497,6 +497,43 @@ jobs: platform: macos backend: ${{ matrix.backend }} + # Merges the per-job envelope artifacts into a single `envelopes-all` download. + # See docs/envelope-capture.md. + collect-envelopes: + name: Collect captured envelopes + runs-on: ubuntu-latest + if: ${{ always() }} + needs: [test-run-android, test-run-ios, test-run-webgl, test-run-linux, test-run-windows, test-run-macos] + steps: + - name: Download all envelope artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: envelopes-* + merge-multiple: true + path: envelopes + + - name: Summarize corpus + run: | + "## Captured envelopes`n" >> $env:GITHUB_STEP_SUMMARY + "| Platform | Payloads | Test actions |" >> $env:GITHUB_STEP_SUMMARY + "| --- | --- | --- |" >> $env:GITHUB_STEP_SUMMARY + $total = 0 + Get-ChildItem -Path envelopes -Recurse -Filter index.jsonl | Sort-Object { $_.Directory.Name } | ForEach-Object { + $entries = Get-Content $_.FullName | ForEach-Object { $_ | ConvertFrom-Json } + $total += $entries.Count + $actions = ($entries | ForEach-Object { $_.label } | Sort-Object -Unique) -join ", " + "| $($_.Directory.Name) | $($entries.Count) | $actions |" >> $env:GITHUB_STEP_SUMMARY + } + "`nTotal: $total payloads" >> $env:GITHUB_STEP_SUMMARY + + - name: Upload combined corpus + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: envelopes-all + path: envelopes/ + if-no-files-found: error + retention-days: 14 + build-size-summary: name: Build Size runs-on: ubuntu-latest diff --git a/.github/workflows/test-run-android.yml b/.github/workflows/test-run-android.yml index 67b6471f6..9a5caf4de 100644 --- a/.github/workflows/test-run-android.yml +++ b/.github/workflows/test-run-android.yml @@ -84,6 +84,13 @@ jobs: mkdir -p $HOME/.android/avd touch $HOME/.android/repositories.cfg + # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which + # points the DSN baked into the test build at 127.0.0.1:8000. `adb reverse` below tunnels that + # port from the device to the runner. See docs/envelope-capture.md. + - name: Start envelope capture server + if: contains(env.SENTRY_DSN, '127.0.0.1') + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" + - name: Run Android Integration Tests (emulator) if: ${{ steps.device.outputs.device == 'emulator' }} uses: reactivecircus/android-emulator-runner@0a638108440efd5c7f980e6ba145dbcdd8f32009 # v2.37.0 @@ -111,6 +118,7 @@ jobs: adb wait-for-device adb shell input keyevent 82 adb devices -l + adb reverse tcp:8000 tcp:8000 pwsh -Command '$env:SENTRY_TEST_PLATFORM = "Android"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk"; Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI' # --- arm64 path: Redroid container on the arm64 runner (no hypervisor needed) --- @@ -168,10 +176,24 @@ jobs: Start-Sleep -Seconds 5 } adb devices -l + adb reverse tcp:8000 tcp:8000 $env:SENTRY_TEST_PLATFORM = "Android" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI + - name: Stop envelope capture server + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + + - name: Upload captured envelopes + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: envelopes-android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }} + path: test/IntegrationTest/envelopes/ + if-no-files-found: warn + retention-days: 14 + - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index 979ee9a45..d00202a28 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -58,6 +58,12 @@ jobs: run: chmod +x samples/IntegrationTest/Build/test.app/Contents/MacOS/IntegrationTest shell: bash + # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which + # points the DSN baked into the test build at 127.0.0.1:8000. See docs/envelope-capture.md. + - name: Start envelope capture server + if: contains(env.SENTRY_DSN, '127.0.0.1') + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" + - name: Run Integration Tests (Linux) if: inputs.platform == 'linux' timeout-minutes: 20 @@ -86,6 +92,19 @@ jobs: $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.exe" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI + - name: Stop envelope capture server + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + + - name: Upload captured envelopes + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: envelopes-${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }} + path: test/IntegrationTest/envelopes/ + if-no-files-found: warn + retention-days: 14 + - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/test-run-ios.yml b/.github/workflows/test-run-ios.yml index 7d1e0bd89..fb44bc5d3 100644 --- a/.github/workflows/test-run-ios.yml +++ b/.github/workflows/test-run-ios.yml @@ -65,6 +65,13 @@ jobs: with: xcode-version: '15.0' # to run iOS 17.0 we need Xcode 15.0 + # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which + # points the DSN baked into the test build at 127.0.0.1:8000. The simulator shares the host + # loopback, so no port forwarding is needed. See docs/envelope-capture.md. + - name: Start envelope capture server + if: contains(env.SENTRY_DSN, '127.0.0.1') + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" + - name: Run iOS Integration Tests id: integration-test timeout-minutes: 20 @@ -76,6 +83,19 @@ jobs: Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI echo "status=success" >> $env:GITHUB_OUTPUT + - name: Stop envelope capture server + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + + - name: Upload captured envelopes + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: envelopes-ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }} + path: test/IntegrationTest/envelopes/ + if-no-files-found: warn + retention-days: 14 + - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/workflows/test-run-webgl.yml b/.github/workflows/test-run-webgl.yml index cff15cb81..51c6e2dbb 100644 --- a/.github/workflows/test-run-webgl.yml +++ b/.github/workflows/test-run-webgl.yml @@ -43,6 +43,13 @@ jobs: run: pip3 install --upgrade selenium shell: bash + # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which + # points the DSN baked into the test build at 127.0.0.1:8000. The capture server answers CORS + # preflights so the browser can post to it cross-origin. See docs/envelope-capture.md. + - name: Start envelope capture server + if: contains(env.SENTRY_DSN, '127.0.0.1') + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "webgl-${{ inputs.unity-version }}" + - name: Run Integration Tests timeout-minutes: 20 run: | @@ -51,6 +58,19 @@ jobs: $env:SENTRY_TEST_UNITY_VERSION = "${{ inputs.unity-version }}" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI + - name: Stop envelope capture server + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + + - name: Upload captured envelopes + if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: envelopes-webgl-${{ inputs.unity-version }} + path: test/IntegrationTest/envelopes/ + if-no-files-found: warn + retention-days: 14 + - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/scripts/replay-envelopes.py b/scripts/replay-envelopes.py new file mode 100644 index 000000000..9ab2141a3 --- /dev/null +++ b/scripts/replay-envelopes.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Replays a captured envelope corpus into a Sentry instance. + +Takes the output of test/Scripts.Integration.Test/envelope-capture-server.py (envelopes and +crashpad minidump uploads produced by the Unity integration tests on every platform) and posts +it to the DSN of your choice - typically a local Sentry. + +By default every replay gets fresh event ids and timestamps shifted to now, so the same corpus +can be replayed repeatedly without events deduplicating or falling outside the ingest window. + +Usage: + replay-envelopes.py --dsn http://@localhost:9000/1 + replay-envelopes.py --dsn ... --include '*crash*' --dry-run +""" + +import argparse +import fnmatch +import json +import sys +import urllib.error +import urllib.request +import uuid +from datetime import datetime, timezone +from pathlib import Path +from urllib.parse import parse_qsl, urlencode, urlparse + +TIMESTAMP_KEYS = {"timestamp", "start_timestamp", "started", "received", "time"} +JSON_ITEM_TYPES = {"event", "transaction", "session", "sessions", "check_in", "log", "feedback", + "user_report", "replay_event", "profile", "client_report"} + + +def parse_envelope(data): + """Splits envelope bytes into (header, [(item_header, payload)]).""" + newline = data.find(b"\n") + if newline == -1: + raise ValueError("no envelope header") + header = json.loads(data[:newline]) + items = [] + pos = newline + 1 + while pos < len(data): + if data[pos:pos + 1] == b"\n": + pos += 1 + continue + newline = data.find(b"\n", pos) + if newline == -1: + break + item_header = json.loads(data[pos:newline]) + pos = newline + 1 + if "length" in item_header: + end = pos + int(item_header["length"]) + else: + end = data.find(b"\n", pos) + if end == -1: + end = len(data) + items.append((item_header, data[pos:end])) + pos = end + return header, items + + +def serialize_envelope(header, items): + out = [json.dumps(header, separators=(",", ":")).encode(), b"\n"] + for item_header, payload in items: + item_header = dict(item_header, length=len(payload)) + out += [json.dumps(item_header, separators=(",", ":")).encode(), b"\n", payload, b"\n"] + return b"".join(out) + + +def to_epoch(value): + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() + except ValueError: + return None + return None + + +def from_epoch(epoch, template): + if isinstance(template, (int, float)): + return epoch + return datetime.fromtimestamp(epoch, timezone.utc).isoformat().replace("+00:00", "Z") + + +def collect_timestamps(node, found): + if isinstance(node, dict): + for key, value in node.items(): + if key in TIMESTAMP_KEYS: + epoch = to_epoch(value) + if epoch: + found.append(epoch) + collect_timestamps(value, found) + elif isinstance(node, list): + for value in node: + collect_timestamps(value, found) + + +def shift_timestamps(node, delta): + if isinstance(node, dict): + for key, value in node.items(): + if key in TIMESTAMP_KEYS: + epoch = to_epoch(value) + if epoch: + node[key] = from_epoch(epoch + delta, value) + continue + shift_timestamps(value, delta) + elif isinstance(node, list): + for value in node: + shift_timestamps(value, delta) + + +def rewrite(data, dsn, new_ids, fresh_timestamps): + header, items = parse_envelope(data) + header["dsn"] = dsn.url + header["sent_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + + parsed = [] + for item_header, payload in items: + if item_header.get("type") in JSON_ITEM_TYPES: + try: + parsed.append((item_header, json.loads(payload), True)) + continue + except (ValueError, UnicodeDecodeError): + pass + parsed.append((item_header, payload, False)) + + if fresh_timestamps: + found = [] + for _, payload, is_json in parsed: + if is_json: + collect_timestamps(payload, found) + if found: + delta = datetime.now(timezone.utc).timestamp() - max(found) + for _, payload, is_json in parsed: + if is_json: + shift_timestamps(payload, delta) + + if new_ids: + event_id = uuid.uuid4().hex + if "event_id" in header: + header["event_id"] = event_id + for _, payload, is_json in parsed: + if is_json and isinstance(payload, dict) and "event_id" in payload: + payload["event_id"] = event_id + + rebuilt = [ + (item_header, json.dumps(payload, separators=(",", ":")).encode() if is_json else payload) + for item_header, payload, is_json in parsed + ] + return serialize_envelope(header, rebuilt) + + +class Dsn: + def __init__(self, url): + parsed = urlparse(url) + if not parsed.username or not parsed.hostname or len(parsed.path) < 2: + raise ValueError(f"not a valid DSN: {url}") + self.url = url + self.key = parsed.username + self.project = parsed.path.strip("/") + port = f":{parsed.port}" if parsed.port else "" + self.base = f"{parsed.scheme}://{parsed.hostname}{port}/api/{self.project}" + + def endpoint(self, name): + return f"{self.base}/{name}/" + + +def post(url, body, content_type, dsn, timeout): + auth = f"Sentry sentry_version=7, sentry_client=replay-envelopes/1.0, sentry_key={dsn.key}" + request = urllib.request.Request( + url, data=body, method="POST", + headers={"Content-Type": content_type, "X-Sentry-Auth": auth}) + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + return response.status, response.read(200).decode("utf-8", "replace") + except urllib.error.HTTPError as error: + return error.code, error.read(400).decode("utf-8", "replace") + except urllib.error.URLError as error: + return None, str(error.reason) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("corpus", help="directory with captured .envelope / .multipart.bin files") + parser.add_argument("--dsn", required=True, help="target DSN, e.g. http://key@localhost:9000/1") + parser.add_argument("--include", default="*", help="glob filter on the file name") + parser.add_argument("--keep-ids", action="store_true", help="replay original event ids") + parser.add_argument("--keep-timestamps", action="store_true", help="do not shift timestamps to now") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--timeout", type=float, default=30) + args = parser.parse_args() + + dsn = Dsn(args.dsn) + corpus = Path(args.corpus) + files = sorted(path for path in corpus.rglob("*") + if path.suffix in (".envelope", ".bin") and fnmatch.fnmatch(path.name, args.include)) + if not files: + print(f"no envelopes matching '{args.include}' under {corpus}", file=sys.stderr) + return 1 + + failures = 0 + for path in files: + data = path.read_bytes() + + if path.name.endswith(".multipart.bin"): + # crashpad minidump upload - replayed verbatim, only the ingest key is swapped + meta = json.loads(path.with_name(path.name[:-len(".multipart.bin")] + ".meta.json").read_text()) + content_type = meta["headers"].get("Content-Type", "multipart/form-data") + query = dict(parse_qsl(meta.get("query", ""))) + query["sentry_key"] = dsn.key + url = f"{dsn.endpoint('minidump')}?{urlencode(query)}" + else: + try: + data = rewrite(data, dsn, not args.keep_ids, not args.keep_timestamps) + except Exception as error: + print(f"SKIP {path.name}: cannot rewrite ({error})", file=sys.stderr) + failures += 1 + continue + content_type = "application/x-sentry-envelope" + url = dsn.endpoint("envelope") + + if args.dry_run: + print(f"DRY {path.name} -> {url} ({len(data)} bytes)") + continue + + status, body = post(url, data, content_type, dsn, args.timeout) + ok = status is not None and 200 <= status < 300 + failures += 0 if ok else 1 + print(f"{'OK ' if ok else 'FAIL'} {status if status else 'ERR'} {path.name} {body.strip()[:120]}") + + print(f"\n{len(files) - failures}/{len(files)} replayed to {dsn.base}") + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 9dec4f8eb..d5c267141 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -132,6 +132,26 @@ BeforeAll { return $runResult } + # Tags the envelopes that envelope-capture-server.py records next with the test action + # they belong to. No-op unless the DSN points at the local capture server. + function Set-CaptureLabel { + param ( + [Parameter(Mandatory=$true)] + [string]$Label + ) + + if (-not $script:CaptureMode) { + return + } + + try { + Invoke-WebRequest -Uri "http://127.0.0.1:8000/MARK?label=$Label" -TimeoutSec 5 -UseBasicParsing | Out-Null + } + catch { + Write-Host "Failed to mark capture label '$Label': $_" + } + } + # Run integration test action function Invoke-TestAction { param ( @@ -140,6 +160,7 @@ BeforeAll { ) Write-Host "Running $Action..." + Set-CaptureLabel -Label $Action if ($script:Platform -eq "WebGL") { return Invoke-WebGLTestAction -Action $Action @@ -160,6 +181,7 @@ BeforeAll { # Launch app again to ensure crash report is sent if ($Action -eq "crash-capture") { Write-Host "Running crash-send to ensure crash report is sent..." + Set-CaptureLabel -Label "crash-send" $sendArgs = Get-AppArguments -Action "crash-send" $sendResult = Invoke-DeviceApp -ExecutablePath $script:ExecutablePath -Arguments $sendArgs @@ -197,7 +219,16 @@ BeforeAll { if ([string]::IsNullOrEmpty($env:SENTRY_DSN)) { throw "SENTRY_DSN environment variable is not set." } - if ([string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { + + # Envelope capture mode: the DSN points at envelope-capture-server.py instead of Sentry, so the + # test actions still run (and their raw envelopes get recorded) but there is no backend to verify + # against. The event assertions below fail by design in this mode - the artifact is the corpus. + $script:CaptureMode = $env:SENTRY_DSN -match '://[^@]*@(127\.0\.0\.1|localhost|10\.0\.2\.2)' + if ($script:CaptureMode) { + Write-Host "Envelope capture mode: DSN points at the local capture server, skipping Sentry API verification." -ForegroundColor Yellow + } + + if (-not $script:CaptureMode -and [string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { throw "SENTRY_AUTH_TOKEN environment variable is not set." } if ([string]::IsNullOrEmpty($env:SENTRY_TEST_APP)) { @@ -277,14 +308,18 @@ BeforeAll { AuthToken = $env:SENTRY_AUTH_TOKEN } - Connect-SentryApi ` - -ApiToken $script:TestSetup.AuthToken ` - -DSN $script:TestSetup.Dsn + if (-not $script:CaptureMode) { + Connect-SentryApi ` + -ApiToken $script:TestSetup.AuthToken ` + -DSN $script:TestSetup.Dsn + } } AfterAll { - Disconnect-SentryApi + if (-not $script:CaptureMode) { + Disconnect-SentryApi + } if ($script:Platform -ne "WebGL") { Disconnect-Device } @@ -299,7 +334,7 @@ Describe "Unity $($env:SENTRY_TEST_PLATFORM) Integration Tests" { $script:runResult = Invoke-TestAction -Action "message-capture" $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId) { + if ($eventId -and -not $script:CaptureMode) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -EventId "$eventId" Write-Host "::endgroup::" @@ -325,7 +360,7 @@ Describe "Unity $($env:SENTRY_TEST_PLATFORM) Integration Tests" { $script:runResult = Invoke-TestAction -Action "exception-capture" $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId) { + if ($eventId -and -not $script:CaptureMode) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -EventId "$eventId" Write-Host "::endgroup::" @@ -387,7 +422,7 @@ if ($env:SENTRY_TEST_PLATFORM -ne "WebGL") { } $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId) { + if ($eventId -and -not $script:CaptureMode) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -TagName "test.crash_id" -TagValue "$eventId" -TimeoutSeconds 300 Write-Host "::endgroup::" @@ -443,7 +478,7 @@ if ($env:SENTRY_TEST_PLATFORM -in "Desktop", "Android" -and -not $isCocoaBackend # The native app-hang event is captured in-proc (same run, no relaunch). Its event ID # is generated natively, so look it up by the unique scope tag the app sets instead. $hangId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($hangId) { + if ($hangId -and -not $script:CaptureMode) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -TagName "test.app_hang_id" -TagValue "$hangId" -TimeoutSeconds 300 Write-Host "::endgroup::" diff --git a/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs b/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs index 4482aada7..d2e687617 100644 --- a/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs +++ b/test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Reflection; +using System.Text.RegularExpressions; +using System.Xml; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; @@ -21,6 +23,14 @@ public void OnPreprocessBuild(BuildReport report) public void OnPostprocessBuild(BuildReport report) { var pathToBuiltProject = report.summary.outputPath; + if (report.summary.platform == BuildTarget.StandaloneOSX) + { + // ATS applies to macOS players too and blocks plain HTTP to an IP literal, which is what + // the envelope capture server is. The iOS module isn't available on macOS build agents, + // so patch the plist as plain XML instead of going through PlistDocument. + AllowArbitraryLoadsInMacPlist(Path.Combine(pathToBuiltProject, "Contents", "Info.plist")); + } + if (report.summary.platform == BuildTarget.iOS) { var plistPath = Path.Combine(pathToBuiltProject, "Info.plist"); @@ -51,4 +61,56 @@ public void OnPostprocessBuild(BuildReport report) File.WriteAllText(plistPath, contents); } } + + private static void AllowArbitraryLoadsInMacPlist(string plistPath) + { + if (!File.Exists(plistPath)) + { + Debug.LogError($"Failed to find the plist at {plistPath}."); + return; + } + + var document = new XmlDocument { XmlResolver = null }; + // Parse (not Ignore) keeps the DOCTYPE in the document; the null resolver keeps us from + // fetching the external DTD Apple references. + using (var reader = XmlReader.Create(plistPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse, XmlResolver = null })) + { + document.Load(reader); + } + + var root = document.SelectSingleNode("/plist/dict"); + if (root is null) + { + Debug.LogError("Failed to find the root in the plist."); + return; + } + + foreach (XmlNode child in root.ChildNodes) + { + if (child.Name == "key" && child.InnerText == "NSAppTransportSecurity") + { + Debug.Log("AllowInsecureHttp: plist already contains NSAppTransportSecurity, nothing to do."); + return; + } + } + + var key = document.CreateElement("key"); + key.InnerText = "NSAppTransportSecurity"; + var value = document.CreateElement("dict"); + var allowKey = document.CreateElement("key"); + allowKey.InnerText = "NSAllowsArbitraryLoads"; + value.AppendChild(allowKey); + value.AppendChild(document.CreateElement("true")); + + root.AppendChild(key); + root.AppendChild(value); + document.Save(plistPath); + + // XmlDocument serializes the DOCTYPE with an empty internal subset (`...PropertyList-1.0.dtd"[]>`) + // which Apple's plist parser rejects. Drop it again. + var patched = Regex.Replace(File.ReadAllText(plistPath), @"(\[]*)\[\]>", "$1>"); + File.WriteAllText(plistPath, patched); + + Debug.Log("AllowInsecureHttp: added NSAllowsArbitraryLoads to the macOS plist."); + } } diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py new file mode 100644 index 000000000..4ce6834b1 --- /dev/null +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Captures raw Sentry envelopes sent by the integration test app. + +Stands in for Sentry's ingest endpoint: accepts every request, writes the body to disk +and answers 200 so the SDK considers the payload delivered. Point the DSN of the test +build at this server (host 127.0.0.1) and the run produces a corpus of real envelopes - +including native crash envelopes and crashpad minidump uploads - that can be replayed +against a local Sentry via scripts/replay-envelopes.py. + +Usage: + envelope-capture-server.py --output DIR [--host 0.0.0.0] [--port 8000] [--platform NAME] + +Control endpoints: + GET /HEALTH 200 once the server is serving + GET /MARK?label=foo tags subsequently captured files with `foo` (the test action) + GET /STOP shuts the server down +""" + +import argparse +import gzip +import json +import re +import sys +import threading +import uuid +import zlib +from datetime import datetime, timezone +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import parse_qs, urlparse + +state_lock = threading.Lock() +sequence = 0 +label = "startup" +output_dir = Path(".") +platform_name = "unknown" + + +def parse_envelope(data): + """Splits envelope bytes into (header, [(item_header, payload)]).""" + newline = data.find(b"\n") + if newline == -1: + raise ValueError("no envelope header") + header = json.loads(data[:newline]) + items = [] + pos = newline + 1 + while pos < len(data): + if data[pos:pos + 1] == b"\n": + pos += 1 + continue + newline = data.find(b"\n", pos) + if newline == -1: + break + item_header = json.loads(data[pos:newline]) + pos = newline + 1 + if "length" in item_header: + end = pos + int(item_header["length"]) + else: + end = data.find(b"\n", pos) + if end == -1: + end = len(data) + items.append((item_header, data[pos:end])) + pos = end + return header, items + + +def decode_body(body, encoding): + if not encoding: + return body + encoding = encoding.lower() + try: + if encoding == "gzip": + return gzip.decompress(body) + if encoding in ("deflate", "zlib"): + return zlib.decompress(body) + except Exception as error: + print(f"failed to decompress {encoding} body: {error}", file=sys.stderr) + return body + + +def safe(value): + return re.sub(r"[^A-Za-z0-9_.-]", "_", value)[:60] or "unknown" + + +class Handler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, fmt, *args): + print(f"{self.address_string()} - {fmt % args}", file=sys.stderr) + + def cors(self): + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "*") + self.send_header("Access-Control-Max-Age", "86400") + + def respond(self, code, payload=b"", content_type="application/json"): + self.send_response(code) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + self.cors() + self.end_headers() + if payload: + self.wfile.write(payload) + + def do_OPTIONS(self): + self.respond(200) + + def do_GET(self): + global label + url = urlparse(self.path) + if url.path == "/HEALTH": + self.respond(200, b'{"ok":true}') + elif url.path == "/MARK": + new_label = parse_qs(url.query).get("label", ["unlabeled"])[0] + with state_lock: + label = safe(new_label) + print(f"--- mark: {label} ---", file=sys.stderr) + self.respond(200, b'{"ok":true}') + elif url.path == "/STOP": + self.respond(200, b'{"ok":true}') + threading.Thread(target=self.server.shutdown).start() + else: + self.respond(200, b"{}") + + def read_body(self): + if self.headers.get("Transfer-Encoding", "").lower() == "chunked": + chunks = [] + while True: + size = int(self.rfile.readline().split(b";")[0], 16) + if size == 0: + self.rfile.readline() + break + chunks.append(self.rfile.read(size)) + self.rfile.readline() + return b"".join(chunks) + return self.rfile.read(int(self.headers.get("Content-Length", 0))) + + def do_POST(self): + global sequence + url = urlparse(self.path) + raw = self.read_body() + body = decode_body(raw, self.headers.get("Content-Encoding")) + + with state_lock: + sequence += 1 + seq, current_label = sequence, label + + meta = { + "sequence": seq, + "label": current_label, + "platform": platform_name, + "received": datetime.now(timezone.utc).isoformat(), + "method": self.command, + "path": url.path, + "query": url.query, + "headers": dict(self.headers), + "raw_bytes": len(raw), + "decoded_bytes": len(body), + } + + content_type = self.headers.get("Content-Type", "") + event_id = None + if "multipart/form-data" in content_type: + # crashpad uploads the minidump to /api//minidump/ as multipart + extension = "multipart.bin" + kind = "minidump" + else: + extension = "envelope" + kind = "envelope" + try: + header, items = parse_envelope(body) + meta["envelope_header"] = header + meta["items"] = [ + { + "type": item_header.get("type"), + "length": len(payload), + "filename": item_header.get("filename"), + "content_type": item_header.get("content_type"), + } + for item_header, payload in items + ] + event_id = header.get("event_id") + types = [i.get("type") or "unknown" for i, _ in items] + if types: + kind = "+".join(dict.fromkeys(types)) + except Exception as error: + meta["parse_error"] = str(error) + + name = f"{seq:03d}-{safe(platform_name)}-{safe(current_label)}-{safe(kind)}" + (output_dir / f"{name}.{extension}").write_bytes(body) + (output_dir / f"{name}.meta.json").write_text(json.dumps(meta, indent=2)) + with state_lock: + with (output_dir / "index.jsonl").open("a") as index: + index.write(json.dumps({"file": f"{name}.{extension}", **meta}) + "\n") + + print(f"captured {name}.{extension} ({len(body)} bytes) {url.path}", file=sys.stderr) + self.respond(200, json.dumps({"id": event_id or uuid.uuid4().hex}).encode()) + + +def main(): + global output_dir, platform_name + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--host", default="0.0.0.0") + parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--output", required=True) + parser.add_argument("--platform", default="unknown") + args = parser.parse_args() + + output_dir = Path(args.output) + output_dir.mkdir(parents=True, exist_ok=True) + platform_name = args.platform + + server = ThreadingHTTPServer((args.host, args.port), Handler) + print(f"envelope capture listening on {args.host}:{args.port} -> {output_dir}", file=sys.stderr) + server.serve_forever() + print(f"envelope capture stopped after {sequence} requests", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/test/Scripts.Integration.Test/start-capture-server.ps1 b/test/Scripts.Integration.Test/start-capture-server.ps1 new file mode 100644 index 000000000..39201ee3a --- /dev/null +++ b/test/Scripts.Integration.Test/start-capture-server.ps1 @@ -0,0 +1,42 @@ +#!/usr/bin/env pwsh +# +# Starts envelope-capture-server.py in the background and waits until it serves. +# Used by the integration test workflows when SENTRY_DSN points at the capture host. + +param( + [string] $Platform = "unknown", + [int] $Port = 8000, + [string] $Output = "" +) + +$ErrorActionPreference = "Stop" + +# One directory per platform so the per-job artifacts can be merged into a single corpus +# without index.jsonl and capture-server.log colliding. +if ([string]::IsNullOrEmpty($Output)) { + $Output = "test/IntegrationTest/envelopes/$Platform" +} + +$python = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } +$server = Join-Path $PSScriptRoot "envelope-capture-server.py" + +New-Item -ItemType Directory -Force -Path $Output | Out-Null +$logPath = Join-Path $Output "capture-server.log" + +Start-Process -FilePath $python ` + -ArgumentList @($server, "--output", $Output, "--port", $Port, "--platform", $Platform) ` + -RedirectStandardError $logPath -NoNewWindow + +for ($i = 1; $i -le 30; $i++) { + try { + Invoke-WebRequest -Uri "http://127.0.0.1:$Port/HEALTH" -TimeoutSec 2 -UseBasicParsing | Out-Null + Write-Host "Envelope capture server is up on port $Port (writing to $Output)" + exit 0 + } + catch { + Start-Sleep -Seconds 1 + } +} + +Get-Content $logPath -ErrorAction SilentlyContinue | Write-Host +throw "Envelope capture server did not come up on port $Port" From 83d560027ceaccfe3b346ddb04585b4e4931e856 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 13 Aug 2026 18:32:48 +0200 Subject: [PATCH 02/18] fix capture port collision and sentry-cli url --- .github/workflows/ci.yml | 24 ++--- .github/workflows/test-run-android.yml | 10 +- .github/workflows/test-run-desktop.yml | 6 +- .github/workflows/test-run-ios.yml | 6 +- .github/workflows/test-run-webgl.yml | 9 +- docs/envelope-capture.md | 91 +++++++++++++++++++ test/IntegrationTest/Integration.Tests.ps1 | 2 +- .../Scripts/CliConfiguration.cs | 5 + .../envelope-capture-server.py | 8 +- .../start-capture-server.ps1 | 2 +- 10 files changed, 136 insertions(+), 27 deletions(-) create mode 100644 docs/envelope-capture.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcc52bb5b..f1e0af370 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -238,7 +238,7 @@ jobs: run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: BUILD_PLATFORM: ${{ matrix.build_platform }} - SENTRY_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md - name: Build Project run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" @@ -294,7 +294,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} @@ -308,7 +308,7 @@ jobs: needs: [test-build-android, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md uses: ./.github/workflows/test-run-android.yml with: unity-version: ${{ matrix.unity-version }} @@ -329,7 +329,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} @@ -362,7 +362,7 @@ jobs: init-type: ${{ matrix.init-type }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -386,7 +386,7 @@ jobs: needs: [test-build-webgl, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -402,7 +402,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -418,7 +418,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -434,7 +434,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -449,7 +449,7 @@ jobs: needs: [test-build-linux, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -467,7 +467,7 @@ jobs: needs: [test-build-windows, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: @@ -485,7 +485,7 @@ jobs: needs: [test-build-macos, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8000/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md strategy: fail-fast: false matrix: diff --git a/.github/workflows/test-run-android.yml b/.github/workflows/test-run-android.yml index 9a5caf4de..176528713 100644 --- a/.github/workflows/test-run-android.yml +++ b/.github/workflows/test-run-android.yml @@ -85,7 +85,7 @@ jobs: touch $HOME/.android/repositories.cfg # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8000. `adb reverse` below tunnels that + # points the DSN baked into the test build at 127.0.0.1:8787. `adb reverse` below tunnels that # port from the device to the runner. See docs/envelope-capture.md. - name: Start envelope capture server if: contains(env.SENTRY_DSN, '127.0.0.1') @@ -118,7 +118,7 @@ jobs: adb wait-for-device adb shell input keyevent 82 adb devices -l - adb reverse tcp:8000 tcp:8000 + adb reverse tcp:8787 tcp:8787 pwsh -Command '$env:SENTRY_TEST_PLATFORM = "Android"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk"; Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI' # --- arm64 path: Redroid container on the arm64 runner (no hypervisor needed) --- @@ -176,14 +176,16 @@ jobs: Start-Sleep -Seconds 5 } adb devices -l - adb reverse tcp:8000 tcp:8000 + adb reverse tcp:8787 tcp:8787 $env:SENTRY_TEST_PLATFORM = "Android" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI - name: Stop envelope capture server if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } - name: Upload captured envelopes if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index d00202a28..14b1312a1 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -59,7 +59,7 @@ jobs: shell: bash # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8000. See docs/envelope-capture.md. + # points the DSN baked into the test build at 127.0.0.1:8787. See docs/envelope-capture.md. - name: Start envelope capture server if: contains(env.SENTRY_DSN, '127.0.0.1') run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" @@ -94,7 +94,9 @@ jobs: - name: Stop envelope capture server if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } - name: Upload captured envelopes if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} diff --git a/.github/workflows/test-run-ios.yml b/.github/workflows/test-run-ios.yml index fb44bc5d3..5510181a9 100644 --- a/.github/workflows/test-run-ios.yml +++ b/.github/workflows/test-run-ios.yml @@ -66,7 +66,7 @@ jobs: xcode-version: '15.0' # to run iOS 17.0 we need Xcode 15.0 # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8000. The simulator shares the host + # points the DSN baked into the test build at 127.0.0.1:8787. The simulator shares the host # loopback, so no port forwarding is needed. See docs/envelope-capture.md. - name: Start envelope capture server if: contains(env.SENTRY_DSN, '127.0.0.1') @@ -85,7 +85,9 @@ jobs: - name: Stop envelope capture server if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } - name: Upload captured envelopes if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} diff --git a/.github/workflows/test-run-webgl.yml b/.github/workflows/test-run-webgl.yml index 51c6e2dbb..f2626708f 100644 --- a/.github/workflows/test-run-webgl.yml +++ b/.github/workflows/test-run-webgl.yml @@ -44,8 +44,9 @@ jobs: shell: bash # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8000. The capture server answers CORS - # preflights so the browser can post to it cross-origin. See docs/envelope-capture.md. + # points the DSN baked into the test build at 127.0.0.1:8787. The capture server answers CORS + # preflights so the browser can post to it cross-origin, and stays off port 8000 because + # webgl-server.py serves the build there. See docs/envelope-capture.md. - name: Start envelope capture server if: contains(env.SENTRY_DSN, '127.0.0.1') run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "webgl-${{ inputs.unity-version }}" @@ -60,7 +61,9 @@ jobs: - name: Stop envelope capture server if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: Invoke-WebRequest -Uri "http://127.0.0.1:8000/STOP" -TimeoutSec 5 -UseBasicParsing -ErrorAction SilentlyContinue | Out-Null + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } - name: Upload captured envelopes if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} diff --git a/docs/envelope-capture.md b/docs/envelope-capture.md new file mode 100644 index 000000000..5cd25f8fc --- /dev/null +++ b/docs/envelope-capture.md @@ -0,0 +1,91 @@ +# Capturing a raw envelope corpus from the integration tests + +The integration tests exercise every managed error, native crash and app hang path the SDK has, on +every platform we ship. Capture mode records what those runs actually put on the wire - raw +envelopes, minidump uploads and all - so the payloads can be replayed against a local Sentry instead +of being sent to sentry.io. + +Two pieces: + +| | | +|---|---| +| [`test/Scripts.Integration.Test/envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for the ingest endpoint. Writes every request to disk and answers `200`. | +| [`scripts/replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured corpus to a DSN of your choice. | + +## Capturing from CI + +**This is a temporary branch change, not a feature.** `ci.yml` hardcodes the DSN to +`http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`, because the DSN is baked into +`SentryOptions.asset` at build time and so has to be switched for the whole pipeline. There is +nothing to toggle: push the branch, open the PR, wait for CI, fetch the artifact. Revert the commit +once you have the corpus. + +Every run job: + +1. starts the capture server on `127.0.0.1:8787` (`adb reverse` tunnels the port on Android; the iOS + simulator and WebGL's headless Chrome share the runner's loopback), +2. runs the usual test actions, marking each captured file with the action it belongs to, +3. uploads its own `envelopes--` artifact. + +The `collect-envelopes` job merges all of them into a single **`envelopes-all`** artifact - one +download for the whole matrix - and writes a per-platform payload count to the run summary: + +```bash +gh run download -n envelopes-all -D ./corpus +``` + +Two things the capture DSN would otherwise break, both handled: + +- `webgl-server.py` serves the WebGL build on port 8000, so the capture server listens on **8787**. +- sentry-cli takes its upload URL from the DSN whenever that DSN is not sentry.io + ([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)), which would point symbol + upload at the capture server. `CliConfiguration` pins `UrlOverride` to `https://sentry.io`. + +**The integration tests fail by design in capture mode.** There is no backend to verify against, so +`Integration.Tests.ps1` skips the Sentry API lookups and every event assertion fails. The artifacts +are the deliverable; a red run is expected. + +Coverage per matrix entry: `message-capture`, `exception-capture`, `crash-capture` (+ the +`crash-send` relaunch that flushes the crash envelope) and `app-hang-capture`, each of which also +emits logs, metrics, sessions and a transaction. Windows/macOS/Linux run twice, once per crash +backend (`crashpad`/`breakpad`/`native`/`cocoa`), so the corpus covers each native payload shape. + +## Capturing locally + +```bash +python3 test/Scripts.Integration.Test/envelope-capture-server.py --output ./envelopes --platform macos +SENTRY_DSN="http://capture@127.0.0.1:8787/1" \ + ./test/Scripts.Integration.Test/dev-integration-test.ps1 -UnityVersion 6000.2 -Platform MacOS +``` + +Any DSN whose host is `127.0.0.1`, `localhost` or `10.0.2.2` puts `Integration.Tests.ps1` into +capture mode. Note that this also applies when you point the tests straight at a locally running +Sentry - the run works, but the API verification is skipped. + +## What lands on disk + +One directory per matrix entry, so the merged corpus stays collision-free: + +``` +macos-cocoa-6000.2/001-macos-cocoa-6000.2-exception-capture-event_attachment.envelope # raw bytes, gunzipped +macos-cocoa-6000.2/001-macos-cocoa-6000.2-exception-capture-event_attachment.meta.json # path, headers, item types +windows-crashpad-6000.2/003-...-crash-capture-minidump.multipart.bin # crashpad minidump upload +windows-crashpad-6000.2/index.jsonl # one line per request +windows-crashpad-6000.2/capture-server.log +``` + +## Replaying into a local Sentry + +```bash +python3 scripts/replay-envelopes.py ./envelopes --dsn http://@localhost:9000/1 +python3 scripts/replay-envelopes.py ./envelopes --dsn ... --include '*crash*' --dry-run +``` + +Each envelope is rewritten before it is posted: the DSN in the envelope header is swapped for the +target, `sent_at` is set to now, event ids are regenerated and all timestamps are shifted to now +while keeping their relative offsets (breadcrumbs, spans, session start). That keeps a corpus +replayable indefinitely without deduplicating against itself or falling outside the ingest window. +Pass `--keep-ids` / `--keep-timestamps` to replay the bytes as they were captured. + +Minidump uploads are replayed verbatim to `/api//minidump/` with only the ingest key +swapped - the event ids inside the multipart body are left alone. diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index d5c267141..7d0d68e08 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -145,7 +145,7 @@ BeforeAll { } try { - Invoke-WebRequest -Uri "http://127.0.0.1:8000/MARK?label=$Label" -TimeoutSec 5 -UseBasicParsing | Out-Null + Invoke-WebRequest -Uri "http://127.0.0.1:8787/MARK?label=$Label" -TimeoutSec 5 -UseBasicParsing | Out-Null } catch { Write-Host "Failed to mark capture label '$Label': $_" diff --git a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs index 89b72cd1f..d6ecbeaa5 100644 --- a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs @@ -16,6 +16,11 @@ public override void Configure(SentryCliOptions cliOptions) cliOptions.Organization = "sentry-sdks"; cliOptions.Project = "sentry-unity-integration-tests"; + // sentry-cli derives its upload URL from the DSN whenever that DSN is not sentry.io. During + // envelope capture the DSN points at the local capture server, which would send symbol + // uploads there too. Pin the CLI to sentry.io so symbol upload keeps working either way. + cliOptions.UrlOverride = "https://sentry.io"; + Debug.Log("Sentry: CliConfiguration::Configure() finished"); } } diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index 4ce6834b1..b44b57c78 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -8,7 +8,7 @@ against a local Sentry via scripts/replay-envelopes.py. Usage: - envelope-capture-server.py --output DIR [--host 0.0.0.0] [--port 8000] [--platform NAME] + envelope-capture-server.py --output DIR [--host 0.0.0.0] [--port 8787] [--platform NAME] Control endpoints: GET /HEALTH 200 once the server is serving @@ -98,6 +98,10 @@ def respond(self, code, payload=b"", content_type="application/json"): self.send_response(code) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(payload))) + # One request per connection. Keep-alive sockets that the server later drops surface as + # "the network connection was lost" in NSURLSession and cost us envelopes. + self.send_header("Connection", "close") + self.close_connection = True self.cors() self.end_headers() if payload: @@ -203,7 +207,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", default="0.0.0.0") - parser.add_argument("--port", type=int, default=8000) + parser.add_argument("--port", type=int, default=8787) parser.add_argument("--output", required=True) parser.add_argument("--platform", default="unknown") args = parser.parse_args() diff --git a/test/Scripts.Integration.Test/start-capture-server.ps1 b/test/Scripts.Integration.Test/start-capture-server.ps1 index 39201ee3a..53ceb3e6a 100644 --- a/test/Scripts.Integration.Test/start-capture-server.ps1 +++ b/test/Scripts.Integration.Test/start-capture-server.ps1 @@ -5,7 +5,7 @@ param( [string] $Platform = "unknown", - [int] $Port = 8000, + [int] $Port = 8787, [string] $Output = "" ) From 4a1329d83bea49bb7cddc22134ce961c95bab9f9 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 13 Aug 2026 19:12:25 +0200 Subject: [PATCH 03/18] capture debug files via sentry-cli mock --- .github/workflows/ci.yml | 106 ---------------- .github/workflows/test-build-android.yml | 55 +++------ .github/workflows/test-build-ios.yml | 40 ------ .github/workflows/test-build-linux.yml | 57 +++------ .github/workflows/test-build-macos.yml | 57 +++------ .github/workflows/test-build-windows.yml | 57 +++------ .github/workflows/test-compile-ios.yml | 66 +++------- docs/envelope-capture.md | 105 +++++++++------- scripts/ci-docker.sh | 2 +- .../Scripts/CliConfiguration.cs | 6 +- .../envelope-capture-server.py | 114 +++++++++++++++++- 11 files changed, 272 insertions(+), 393 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1e0af370..ec69ae5ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,46 +73,6 @@ jobs: with: unity-version: ${{ matrix.unity-version }} - package-validation: - name: UPM Package validation - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ github.head_ref || github.ref }} - ssh-key: ${{ secrets.CI_DEPLOY_KEY }} - - - name: Download UPM package - uses: ./.github/actions/wait-for-artifact - with: - name: package-release - - - name: Check snapshot - id: snapshot-check - shell: pwsh - run: | - $ErrorActionPreference = 'Continue' - & ./test/Scripts.Tests/test-pack-contents.ps1 - "result=$LASTEXITCODE" >> $env:GITHUB_OUTPUT - - - name: Update snapshot for dependency bumps - if: steps.snapshot-check.outputs.result != '0' && github.event_name == 'pull_request' && startsWith(github.head_ref, 'deps/') - run: | - ./test/Scripts.Tests/test-pack-contents.ps1 accept - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add test/Scripts.Tests/package-release.zip.snapshot - git commit -m "Update package release snapshot" - git push - - - name: Fail on unexpected snapshot changes - if: steps.snapshot-check.outputs.result != '0' && !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'deps/')) - run: | - Write-Error "Package contents do not match snapshot. Run './test/Scripts.Tests/test-pack-contents.ps1 accept' locally and commit the update." - exit 1 - - # This produces the `samples/IntegrationTest` as `test-${{ matrix.unity-version }}`. test-create: name: Create ${{ matrix.unity-version }} Test Project if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} @@ -189,26 +149,6 @@ jobs: restore-keys: | it-library-ubuntu-${{ matrix.unity-version }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-${{ matrix.build_platform }}-${{ matrix.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -BuildDirName "Build-NoSentry" - env: - BUILD_PLATFORM: ${{ matrix.build_platform }} - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-${{ matrix.build_platform }}-${{ matrix.unity-version }} - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -220,20 +160,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - if: ${{ !startsWith(matrix.unity-version, '2021') }} - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - - - name: Disable DependencyConflict (WebGL 2021) - if: ${{ startsWith(matrix.unity-version, '2021') }} - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable - - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: @@ -246,20 +172,6 @@ jobs: BUILD_PLATFORM: ${{ matrix.build_platform }} UNITY_VERSION: ${{ matrix.unity-version }} - - name: Compare build sizes - run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" - env: - BUILD_PLATFORM: ${{ matrix.build_platform }} - UNITY_VERSION: ${{ matrix.unity-version }} - - - name: Upload build size measurement - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: build-size-${{ matrix.platform }}-${{ matrix.unity-version }} - path: build-size-measurements/*.json - retention-days: 1 - - # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -534,21 +446,3 @@ jobs: if-no-files-found: error retention-days: 14 - build-size-summary: - name: Build Size - runs-on: ubuntu-latest - if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} - needs: [test-build-webgl, test-build-android, test-compile-ios, test-build-linux, test-build-windows, test-build-macos] - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Download all build size measurements - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: build-size-* - path: build-size-measurements - - - name: Create consolidated summary - shell: pwsh - run: ./scripts/create-build-size-summary.ps1 \ No newline at end of file diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 1524018ed..510a3431f 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -60,24 +60,6 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Android-${{ inputs.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -BuildDirName "Build-NoSentry" - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Android-${{ inputs.unity-version }} - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -89,14 +71,10 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it + # uploads during the build lands in the capture output. See docs/envelope-capture.md. + - name: Start envelope capture server + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" @@ -109,16 +87,6 @@ jobs: - name: Assert symbols and sources were uploaded (Runtime) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log - - name: Compare build sizes (Runtime) - run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" - - - name: Upload build size measurement - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: build-size-Android-${{ env.UNITY_VERSION }} - path: build-size-measurements/*.json - retention-days: 1 - - name: Bundle build & symbol-upload logs with the APK (Runtime) run: | Copy-Item unity.log samples/IntegrationTest/Build/ -ErrorAction SilentlyContinue @@ -176,3 +144,18 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 # Lower retention period - we only need this to retry CI. + - name: Stop envelope capture server + if: ${{ always() }} + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } + + - name: Upload captured debug files + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: symbols-android-${{ env.UNITY_VERSION }} + path: test/IntegrationTest/symbols/ + if-no-files-found: warn + retention-days: 14 + diff --git a/.github/workflows/test-build-ios.yml b/.github/workflows/test-build-ios.yml index 9dab592ec..c30fe38c1 100644 --- a/.github/workflows/test-build-ios.yml +++ b/.github/workflows/test-build-ios.yml @@ -66,37 +66,6 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-iOS-${{ inputs.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "iOS" -BuildDirName "Build-NoSentry" - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-iOS-${{ inputs.unity-version }} - - - name: Create archive for build without Sentry - shell: bash - run: | - rm -rf samples/IntegrationTest/Build-NoSentry/*_BackUpThisFolder_ButDontShipItWithYourGame - tar -cvzf test-app-no-sentry.tar.gz samples/IntegrationTest/Build-NoSentry - - - name: Upload build without Sentry - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: testapp-ios-no-sentry-${{ env.UNITY_VERSION }} - path: test-app-no-sentry.tar.gz - retention-days: 1 - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -108,15 +77,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 5535dc050..15cd4ff8d 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -71,24 +71,6 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Linux-${{ inputs.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -BuildDirName "Build-NoSentry" - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Linux-${{ inputs.unity-version }} - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -100,14 +82,10 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it + # uploads during the build lands in the capture output. See docs/envelope-capture.md. + - name: Start envelope capture server + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux @@ -120,17 +98,6 @@ jobs: - name: Assert symbols and sources were uploaded (Breakpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log - - name: Compare build sizes - run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform Linux -UnityVersion "$env:UNITY_VERSION" - - - name: Upload build size measurement - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: build-size-Linux-${{ env.UNITY_VERSION }} - path: build-size-measurements/*.json - retention-days: 1 - - # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Breakpad backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -203,3 +170,19 @@ jobs: unity.log !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + + - name: Stop envelope capture server + if: ${{ always() }} + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } + + - name: Upload captured debug files + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: symbols-linux-${{ env.UNITY_VERSION }} + path: test/IntegrationTest/symbols/ + if-no-files-found: warn + retention-days: 14 + diff --git a/.github/workflows/test-build-macos.yml b/.github/workflows/test-build-macos.yml index 318f71bd5..78840ce5b 100644 --- a/.github/workflows/test-build-macos.yml +++ b/.github/workflows/test-build-macos.yml @@ -66,24 +66,6 @@ jobs: restore-keys: | it-library-macos-${{ env.UNITY_VERSION }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-MacOS-${{ inputs.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -BuildDirName "Build-NoSentry" - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-MacOS-${{ inputs.unity-version }} - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -95,14 +77,10 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it + # uploads during the build lands in the capture output. See docs/envelope-capture.md. + - name: Start envelope capture server + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS @@ -117,17 +95,6 @@ jobs: - name: Assert symbols and sources were uploaded (Cocoa backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log - - name: Compare build sizes - run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" - - - name: Upload build size measurement - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: build-size-MacOS-${{ env.UNITY_VERSION }} - path: build-size-measurements/*.json - retention-days: 1 - - # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Cocoa backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -189,3 +156,19 @@ jobs: unity.log !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + + - name: Stop envelope capture server + if: ${{ always() }} + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } + + - name: Upload captured debug files + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: symbols-macos-${{ env.UNITY_VERSION }} + path: test/IntegrationTest/symbols/ + if-no-files-found: warn + retention-days: 14 + diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml index 89dedc474..cba3c0d94 100644 --- a/.github/workflows/test-build-windows.yml +++ b/.github/workflows/test-build-windows.yml @@ -66,24 +66,6 @@ jobs: restore-keys: | it-library-windows-${{ env.UNITY_VERSION }}- - - name: Restore cached build without Sentry - id: cache-build-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Windows-${{ inputs.unity-version }} - - - name: Build without Sentry SDK - if: steps.cache-build-nosentry.outputs.cache-hit != 'true' - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -BuildDirName "Build-NoSentry" - - - name: Save cached build without Sentry - if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: samples/IntegrationTest/Build-NoSentry - key: build-nosentry-Windows-${{ inputs.unity-version }} - - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -95,14 +77,10 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - - name: Download DependencyConflict package - uses: ./.github/actions/wait-for-artifact - with: - name: dependency-conflict-package - path: dependency-conflict-package - - - name: Add DependencyConflict to the project - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it + # uploads during the build lands in the capture output. See docs/envelope-capture.md. + - name: Start envelope capture server + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows @@ -117,17 +95,6 @@ jobs: - name: Assert symbols and sources were uploaded (Crashpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log - - name: Compare build sizes - run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform Windows -UnityVersion "$env:UNITY_VERSION" - - - name: Upload build size measurement - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: build-size-Windows-${{ env.UNITY_VERSION }} - path: build-size-measurements/*.json - retention-days: 1 - - # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Crashpad backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -189,3 +156,19 @@ jobs: unity.log !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + + - name: Stop envelope capture server + if: ${{ always() }} + run: | + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } + + - name: Upload captured debug files + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: symbols-windows-${{ env.UNITY_VERSION }} + path: test/IntegrationTest/symbols/ + if-no-files-found: warn + retention-days: 14 + diff --git a/.github/workflows/test-compile-ios.yml b/.github/workflows/test-compile-ios.yml index 74f58a129..38d282313 100644 --- a/.github/workflows/test-compile-ios.yml +++ b/.github/workflows/test-compile-ios.yml @@ -25,45 +25,6 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Restore cached compiled iOS build without Sentry - if: ${{ inputs.init-type == 'runtime' }} - id: cache-compiled-nosentry - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: IntegrationTest-NoSentry.app - key: build-nosentry-iOS-compiled-${{ inputs.unity-version }} - - - name: Download build without Sentry (for size comparison) - if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: testapp-ios-no-sentry-${{ env.UNITY_VERSION }} - - - name: Extract and compile build without Sentry - if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} - run: | - tar -xvzf test-app-no-sentry.tar.gz - Move-Item -Path "samples/IntegrationTest/Build-NoSentry" -Destination "samples/IntegrationTest/Build" - - - name: Compile Xcode project without Sentry - if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} - run: ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" - timeout-minutes: 20 - - - name: Save compiled app without Sentry - if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} - run: | - Copy-Item -Path "samples/IntegrationTest/Build/archive/Unity-iPhone/Build/Products/Release-iphonesimulator/IntegrationTest.app" ` - -Destination "IntegrationTest-NoSentry.app" -Recurse - Remove-Item -Path "samples/IntegrationTest/Build" -Recurse -Force - - - name: Save cached compiled iOS build without Sentry - if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' }} - uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: IntegrationTest-NoSentry.app - key: build-nosentry-iOS-compiled-${{ inputs.unity-version }} - - name: Download app project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -72,6 +33,11 @@ jobs: - name: Extract app project run: tar -xvzf "test-app-$env:INIT_TYPE.tar.gz" + # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it + # uploads during the build lands in the capture output. See docs/envelope-capture.md. + - name: Start envelope capture server + run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" -Output "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" + - name: iOS test run: ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" timeout-minutes: 20 @@ -106,19 +72,17 @@ jobs: # Lower retention period - we only need this to retry CI. retention-days: 14 - - name: Compare build sizes - if: ${{ inputs.init-type == 'runtime' }} + - name: Stop envelope capture server + if: ${{ always() }} run: | - ./test/Scripts.Integration.Test/measure-build-size.ps1 ` - -Path1 "IntegrationTest-NoSentry.app" ` - -Path2 "samples/IntegrationTest/Build/archive/Unity-iPhone/Build/Products/Release-iphonesimulator/IntegrationTest.app" ` - -Platform "iOS" ` - -UnityVersion "$env:UNITY_VERSION" + try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } + catch { Write-Host "Capture server already gone" } - - name: Upload build size measurement - if: ${{ inputs.init-type == 'runtime' }} + - name: Upload captured debug files + if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: build-size-iOS-${{ env.UNITY_VERSION }} - path: build-size-measurements/*.json - retention-days: 1 + name: symbols-ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }} + path: test/IntegrationTest/symbols/ + if-no-files-found: warn + retention-days: 14 diff --git a/docs/envelope-capture.md b/docs/envelope-capture.md index 5cd25f8fc..c21897253 100644 --- a/docs/envelope-capture.md +++ b/docs/envelope-capture.md @@ -1,86 +1,93 @@ -# Capturing a raw envelope corpus from the integration tests +# Capturing a raw envelope + debug file corpus from the integration tests The integration tests exercise every managed error, native crash and app hang path the SDK has, on every platform we ship. Capture mode records what those runs actually put on the wire - raw -envelopes, minidump uploads and all - so the payloads can be replayed against a local Sentry instead -of being sent to sentry.io. - -Two pieces: +envelopes, minidump uploads, and the debug files sentry-cli uploads at build time - so the whole lot +can be replayed against a local Sentry instead of going to sentry.io. | | | |---|---| -| [`test/Scripts.Integration.Test/envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for the ingest endpoint. Writes every request to disk and answers `200`. | -| [`scripts/replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured corpus to a DSN of your choice. | - -## Capturing from CI +| [`test/Scripts.Integration.Test/envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for both Sentry endpoints: envelope ingest, and the chunk-upload API sentry-cli uses for debug files. Writes everything to disk. | +| [`scripts/replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured envelope corpus to a DSN of your choice. | **This is a temporary branch change, not a feature.** `ci.yml` hardcodes the DSN to -`http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`, because the DSN is baked into -`SentryOptions.asset` at build time and so has to be switched for the whole pipeline. There is -nothing to toggle: push the branch, open the PR, wait for CI, fetch the artifact. Revert the commit -once you have the corpus. +`http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`. That one switch redirects both +halves of the capture, because the DSN is what decides where each goes: + +- the SDK sends envelopes there at **run** time, +- sentry-cli derives its upload URL from that same DSN ([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)), + so debug files land there at **build** time. -Every run job: +There is nothing to toggle: push the branch, open the PR, wait for CI, fetch the artifacts. Revert +the commit once you have the corpus. -1. starts the capture server on `127.0.0.1:8787` (`adb reverse` tunnels the port on Android; the iOS - simulator and WebGL's headless Chrome share the runner's loopback), -2. runs the usual test actions, marking each captured file with the action it belongs to, -3. uploads its own `envelopes--` artifact. +## What CI produces -The `collect-envelopes` job merges all of them into a single **`envelopes-all`** artifact - one -download for the whole matrix - and writes a per-platform payload count to the run summary: +| Artifact | From | Contents | +|---|---|---| +| `envelopes-all` | run jobs, merged | every envelope and minidump upload, one directory per platform | +| `symbols--` | build jobs | the debug files and source bundles sentry-cli uploaded for that build | ```bash gh run download -n envelopes-all -D ./corpus +gh run download -p 'symbols-*' -D ./corpus/symbols ``` -Two things the capture DSN would otherwise break, both handled: - -- `webgl-server.py` serves the WebGL build on port 8000, so the capture server listens on **8787**. -- sentry-cli takes its upload URL from the DSN whenever that DSN is not sentry.io - ([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)), which would point symbol - upload at the capture server. `CliConfiguration` pins `UrlOverride` to `https://sentry.io`. +Debug files are big (IL2CPP `GameAssembly.pdb` and friends), so they stay per-platform rather than +being merged into one download. **The integration tests fail by design in capture mode.** There is no backend to verify against, so `Integration.Tests.ps1` skips the Sentry API lookups and every event assertion fails. The artifacts -are the deliverable; a red run is expected. +are the deliverable; a red run is expected. The symbol-upload assertions are the exception and still +mean something: they pass only if sentry-cli really did upload debug files to the capture server. Coverage per matrix entry: `message-capture`, `exception-capture`, `crash-capture` (+ the `crash-send` relaunch that flushes the crash envelope) and `app-hang-capture`, each of which also emits logs, metrics, sessions and a transaction. Windows/macOS/Linux run twice, once per crash backend (`crashpad`/`breakpad`/`native`/`cocoa`), so the corpus covers each native payload shape. -## Capturing locally +Two details the capture DSN would otherwise break, both handled: `webgl-server.py` serves the WebGL +build on port 8000, so capture listens on **8787**; and the Linux/Android/WebGL builds run Unity +inside a container, so [`ci-docker.sh`](../scripts/ci-docker.sh) uses `--network host` to let the +in-container sentry-cli reach the capture server. -```bash -python3 test/Scripts.Integration.Test/envelope-capture-server.py --output ./envelopes --platform macos -SENTRY_DSN="http://capture@127.0.0.1:8787/1" \ - ./test/Scripts.Integration.Test/dev-integration-test.ps1 -UnityVersion 6000.2 -Platform MacOS -``` +### What CI no longer does on this branch -Any DSN whose host is `127.0.0.1`, `localhost` or `10.0.2.2` puts `Integration.Tests.ps1` into -capture mode. Note that this also applies when you point the tests straight at a locally running -Sentry - the run works, but the API verification is skipped. +Stripped to keep the run short and the failures meaningful: the UPM package snapshot validation, all +build-size measurement (including every "build without Sentry" pass and the `build-size-summary` +job), and the dependency-conflict package steps. ## What lands on disk One directory per matrix entry, so the merged corpus stays collision-free: ``` -macos-cocoa-6000.2/001-macos-cocoa-6000.2-exception-capture-event_attachment.envelope # raw bytes, gunzipped -macos-cocoa-6000.2/001-macos-cocoa-6000.2-exception-capture-event_attachment.meta.json # path, headers, item types -windows-crashpad-6000.2/003-...-crash-capture-minidump.multipart.bin # crashpad minidump upload -windows-crashpad-6000.2/index.jsonl # one line per request -windows-crashpad-6000.2/capture-server.log +macos-cocoa-6000.5/001-macos-cocoa-6000.5-exception-capture-event_attachment.envelope # raw bytes, gunzipped +macos-cocoa-6000.5/001-macos-cocoa-6000.5-exception-capture-event_attachment.meta.json # path, headers, item types +windows-crashpad-6000.5/003-...-crash-capture-minidump.multipart.bin # crashpad minidump upload +windows-crashpad-6000.5/index.jsonl # one line per request +symbols/macos-6000.5/debug-files/--GameAssembly.dylib # debug companion +symbols/macos-6000.5/debug-files/--GameAssembly.dylib.src # source bundle +symbols/macos-6000.5/debug-files/index.jsonl # debug id -> file ``` +A dif and its source bundle share a debug id *and* a name, so the checksum in the file name is what +keeps them apart. + ## Replaying into a local Sentry ```bash -python3 scripts/replay-envelopes.py ./envelopes --dsn http://@localhost:9000/1 -python3 scripts/replay-envelopes.py ./envelopes --dsn ... --include '*crash*' --dry-run +# events, crashes, sessions, logs +python3 scripts/replay-envelopes.py ./corpus --dsn http://@localhost:9000/1 + +# the debug files that symbolicate them +sentry-cli --url http://localhost:9000 --auth-token debug-files upload \ + -o -p ./corpus/symbols/macos-6000.5/debug-files ``` +sentry-cli reads the captured files straight out of the artifact and re-uploads them under their +original debug ids, which is what lets the replayed crashes symbolicate. + Each envelope is rewritten before it is posted: the DSN in the envelope header is swapped for the target, `sent_at` is set to now, event ids are regenerated and all timestamps are shifted to now while keeping their relative offsets (breadcrumbs, spans, session start). That keeps a corpus @@ -89,3 +96,15 @@ Pass `--keep-ids` / `--keep-timestamps` to replay the bytes as they were capture Minidump uploads are replayed verbatim to `/api//minidump/` with only the ingest key swapped - the event ids inside the multipart body are left alone. + +## Capturing locally + +```bash +python3 test/Scripts.Integration.Test/envelope-capture-server.py --output ./out --platform macos +SENTRY_DSN="http://capture@127.0.0.1:8787/1" \ + ./test/Scripts.Integration.Test/dev-integration-test.ps1 -UnityVersion 6000.5 -Platform MacOS +``` + +Any DSN whose host is `127.0.0.1`, `localhost` or `10.0.2.2` puts `Integration.Tests.ps1` into +capture mode. Note that this also applies when you point the tests straight at a locally running +Sentry - the run works, but the API verification is skipped. diff --git a/scripts/ci-docker.sh b/scripts/ci-docker.sh index a9330c0b1..c3d8cc8af 100755 --- a/scripts/ci-docker.sh +++ b/scripts/ci-docker.sh @@ -34,7 +34,7 @@ uniqueHostname=$(echo "$uniqueHostname" | tr '[:upper:]_ ' '[:lower:]--' | tr -s # We use the host dotnet installation - it's much faster than installing inside the docker container. set -x docker run -td --name $container \ - --hostname $uniqueHostname \ + --network host \ --user $uid:$gid \ -v "$cwd":/sentry-unity \ -v $ANDROID_HOME:$ANDROID_HOME \ diff --git a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs index d6ecbeaa5..6c7c46d6f 100644 --- a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs @@ -16,10 +16,8 @@ public override void Configure(SentryCliOptions cliOptions) cliOptions.Organization = "sentry-sdks"; cliOptions.Project = "sentry-unity-integration-tests"; - // sentry-cli derives its upload URL from the DSN whenever that DSN is not sentry.io. During - // envelope capture the DSN points at the local capture server, which would send symbol - // uploads there too. Pin the CLI to sentry.io so symbol upload keeps working either way. - cliOptions.UrlOverride = "https://sentry.io"; + // No UrlOverride on purpose: sentry-cli derives its upload URL from the DSN, so during + // envelope capture the debug files go to the capture server and end up in the artifact. Debug.Log("Sentry: CliConfiguration::Configure() finished"); } diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index b44b57c78..8705b48f6 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -20,7 +20,9 @@ import gzip import json import re +import shutil import sys +import tempfile import threading import uuid import zlib @@ -33,6 +35,8 @@ sequence = 0 label = "startup" output_dir = Path(".") +chunk_dir = Path(".") +symbol_dir = Path(".") platform_name = "unknown" @@ -64,6 +68,23 @@ def parse_envelope(data): return header, items +def parse_multipart(body, boundary): + """Yields (headers, payload) for each part of a multipart/form-data body.""" + for segment in body.split(b"--" + boundary): + if segment in (b"", b"--", b"--\r\n", b"\r\n"): + continue + segment = segment[2:] if segment.startswith(b"\r\n") else segment + head, _, payload = segment.partition(b"\r\n\r\n") + if payload.endswith(b"\r\n"): + payload = payload[:-2] + headers = {} + for line in head.decode("utf-8", "replace").splitlines(): + key, sep, value = line.partition(":") + if sep: + headers[key.strip().lower()] = value.strip() + yield headers, payload + + def decode_body(body, encoding): if not encoding: return body @@ -121,6 +142,22 @@ def do_GET(self): label = safe(new_label) print(f"--- mark: {label} ---", file=sys.stderr) self.respond(200, b'{"ok":true}') + elif url.path.endswith("/chunk-upload/"): + # sentry-cli asks what the server accepts before uploading debug files. Advertising + # uncompressed chunks keeps the upload handler trivial. + options = { + "url": f"http://{self.headers.get('Host', '127.0.0.1')}{url.path}", + "chunkSize": 8 * 1024 * 1024, + "chunksPerRequest": 64, + "maxFileSize": 2 * 1024 * 1024 * 1024, + "maxRequestSize": 32 * 1024 * 1024, + "concurrency": 1, + "hashAlgorithm": "sha1", + "compression": [], + "accept": ["debug_files", "sources", "pdbs", "portablepdbs", "il2cpp", + "bcsymbolmaps", "proguard"], + } + self.respond(200, json.dumps(options).encode()) elif url.path == "/STOP": self.respond(200, b'{"ok":true}') threading.Thread(target=self.server.shutdown).start() @@ -140,12 +177,80 @@ def read_body(self): return b"".join(chunks) return self.rfile.read(int(self.headers.get("Content-Length", 0))) + def handle_chunk_upload(self, body): + """Stores each uploaded chunk under its sha1 so assemble can stitch the file back.""" + boundary = re.search(r"boundary=([^;]+)", self.headers.get("Content-Type", "")) + if not boundary: + self.respond(400, b'{"detail":"missing boundary"}') + return + + count = 0 + for headers, payload in parse_multipart(body, boundary.group(1).strip('"').encode()): + name = re.search(r'filename="([^"]*)"', headers.get("content-disposition", "")) + if not name: + continue + (chunk_dir / name.group(1)).write_bytes(payload) + count += 1 + + print(f"stored {count} chunks", file=sys.stderr) + self.respond(200, b"{}") + + def handle_assemble(self, body): + """Reassembles uploaded chunks into the debug files sentry-cli meant to upload.""" + try: + request = json.loads(body) + except ValueError as error: + self.respond(400, json.dumps({"detail": str(error)}).encode()) + return + + response = {} + for checksum, entry in request.items(): + name = Path(entry.get("name") or checksum).name + missing = [c for c in entry.get("chunks", []) if not (chunk_dir / c).exists()] + if missing: + response[checksum] = {"state": "not_found", "missingChunks": missing, "detail": None} + continue + + # A dif and its source bundle share both debug id and name, so the checksum keeps + # them from overwriting each other. + target = symbol_dir / f"{entry.get('debug_id', 'unknown')}-{checksum[:8]}-{safe(name)}" + with target.open("wb") as out: + for chunk in entry["chunks"]: + out.write((chunk_dir / chunk).read_bytes()) + # Source bundles carry Sentry's "SYSB" magic; mark them so the corpus is self-describing. + with target.open("rb") as probe: + if probe.read(4) == b"SYSB": + target = target.rename(target.with_name(target.name + ".src")) + + # IL2CPP debug files run to gigabytes; dropping the chunks once assembled keeps peak + # disk at one copy. sentry-cli re-uploads any chunk a later probe reports missing. + for chunk in entry["chunks"]: + (chunk_dir / chunk).unlink(missing_ok=True) + print(f"assembled {target.name} ({target.stat().st_size} bytes)", file=sys.stderr) + + with state_lock: + with (symbol_dir / "index.jsonl").open("a") as index: + index.write(json.dumps({"file": target.name, "platform": platform_name, + "checksum": checksum, "size": target.stat().st_size, + "request": entry}) + "\n") + + response[checksum] = {"state": "ok", "missingChunks": [], "detail": None} + + self.respond(200, json.dumps(response).encode()) + def do_POST(self): global sequence url = urlparse(self.path) raw = self.read_body() body = decode_body(raw, self.headers.get("Content-Encoding")) + if url.path.endswith("/chunk-upload/"): + self.handle_chunk_upload(body) + return + if url.path.endswith("/assemble/"): + self.handle_assemble(body) + return + with state_lock: sequence += 1 seq, current_label = sequence, label @@ -203,7 +308,7 @@ def do_POST(self): def main(): - global output_dir, platform_name + global output_dir, chunk_dir, symbol_dir, platform_name parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--host", default="0.0.0.0") @@ -216,9 +321,16 @@ def main(): output_dir.mkdir(parents=True, exist_ok=True) platform_name = args.platform + # Debug files sentry-cli uploads land next to the envelopes; the chunks they are stitched + # from are scratch and get cleaned up on shutdown. + symbol_dir = output_dir / "debug-files" + symbol_dir.mkdir(exist_ok=True) + chunk_dir = Path(tempfile.mkdtemp(prefix="sentry-chunks-")) + server = ThreadingHTTPServer((args.host, args.port), Handler) print(f"envelope capture listening on {args.host}:{args.port} -> {output_dir}", file=sys.stderr) server.serve_forever() + shutil.rmtree(chunk_dir, ignore_errors=True) print(f"envelope capture stopped after {sequence} requests", file=sys.stderr) From 0a89476b0d872f80e9c5b8a4b5342232ef4c1424 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 17 Aug 2026 10:11:10 +0200 Subject: [PATCH 04/18] compile out dependency conflict check --- .github/workflows/ci.yml | 5 +++++ .github/workflows/test-build-android.yml | 5 +++++ .github/workflows/test-build-ios.yml | 5 +++++ .github/workflows/test-build-linux.yml | 5 +++++ .github/workflows/test-build-macos.yml | 5 +++++ .github/workflows/test-build-windows.yml | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec69ae5ea..8633dfaac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,6 +160,11 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 510a3431f..285d456ba 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -76,6 +76,11 @@ jobs: - name: Start envelope capture server run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" env: diff --git a/.github/workflows/test-build-ios.yml b/.github/workflows/test-build-ios.yml index c30fe38c1..7fe7aa7fe 100644 --- a/.github/workflows/test-build-ios.yml +++ b/.github/workflows/test-build-ios.yml @@ -77,6 +77,11 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 15cd4ff8d..6b68101ee 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -87,6 +87,11 @@ jobs: - name: Start envelope capture server run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux env: diff --git a/.github/workflows/test-build-macos.yml b/.github/workflows/test-build-macos.yml index 78840ce5b..78962853f 100644 --- a/.github/workflows/test-build-macos.yml +++ b/.github/workflows/test-build-macos.yml @@ -82,6 +82,11 @@ jobs: - name: Start envelope capture server run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS env: diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml index cba3c0d94..2dffdacb4 100644 --- a/.github/workflows/test-build-windows.yml +++ b/.github/workflows/test-build-windows.yml @@ -82,6 +82,11 @@ jobs: - name: Start envelope capture server run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" + # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; + # capture runs don't need that regression check, so compile it out instead of installing it. + - name: Disable DependencyConflict + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows env: From 0a80a06cbb22d92320b8ef1577f4f22392ade008 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 17 Aug 2026 10:54:14 +0200 Subject: [PATCH 05/18] point sentry-cli at capture server via SENTRY_URL --- .github/workflows/test-build-android.yml | 14 ++++++++++++++ .github/workflows/test-build-linux.yml | 14 ++++++++++++++ .github/workflows/test-build-macos.yml | 14 ++++++++++++++ .github/workflows/test-build-windows.yml | 14 ++++++++++++++ .github/workflows/test-compile-ios.yml | 14 ++++++++++++++ scripts/ci-docker.sh | 1 + 6 files changed, 71 insertions(+) diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 285d456ba..1caef190a 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -25,6 +25,9 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} + # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not + # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. + SENTRY_URL: http://127.0.0.1:8787 steps: - name: Checkout @@ -149,6 +152,17 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 # Lower retention period - we only need this to retry CI. + # sentry-cli reporting success is not proof it reached us - it silently falls back to + # sentry.io. Fail loudly when nothing was captured. + - name: Assert debug files were captured + run: | + $dir = "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}/debug-files" + $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) + if ($files.Count -eq 0) { + throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." + } + Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" + - name: Stop envelope capture server if: ${{ always() }} run: | diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 6b68101ee..b492b4b79 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -26,6 +26,9 @@ jobs: UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Linux + # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not + # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. + SENTRY_URL: http://127.0.0.1:8787 steps: - name: Checkout @@ -176,6 +179,17 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + # sentry-cli reporting success is not proof it reached us - it silently falls back to + # sentry.io. Fail loudly when nothing was captured. + - name: Assert debug files were captured + run: | + $dir = "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}/debug-files" + $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) + if ($files.Count -eq 0) { + throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." + } + Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" + - name: Stop envelope capture server if: ${{ always() }} run: | diff --git a/.github/workflows/test-build-macos.yml b/.github/workflows/test-build-macos.yml index 78962853f..62532e110 100644 --- a/.github/workflows/test-build-macos.yml +++ b/.github/workflows/test-build-macos.yml @@ -24,6 +24,9 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: MacOS + # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not + # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. + SENTRY_URL: http://127.0.0.1:8787 steps: - name: Checkout @@ -162,6 +165,17 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + # sentry-cli reporting success is not proof it reached us - it silently falls back to + # sentry.io. Fail loudly when nothing was captured. + - name: Assert debug files were captured + run: | + $dir = "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}/debug-files" + $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) + if ($files.Count -eq 0) { + throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." + } + Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" + - name: Stop envelope capture server if: ${{ always() }} run: | diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml index 2dffdacb4..5f0d18612 100644 --- a/.github/workflows/test-build-windows.yml +++ b/.github/workflows/test-build-windows.yml @@ -24,6 +24,9 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Windows + # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not + # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. + SENTRY_URL: http://127.0.0.1:8787 steps: - name: Checkout @@ -162,6 +165,17 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 + # sentry-cli reporting success is not proof it reached us - it silently falls back to + # sentry.io. Fail loudly when nothing was captured. + - name: Assert debug files were captured + run: | + $dir = "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}/debug-files" + $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) + if ($files.Count -eq 0) { + throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." + } + Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" + - name: Stop envelope capture server if: ${{ always() }} run: | diff --git a/.github/workflows/test-compile-ios.yml b/.github/workflows/test-compile-ios.yml index 38d282313..e1f09e215 100644 --- a/.github/workflows/test-compile-ios.yml +++ b/.github/workflows/test-compile-ios.yml @@ -20,6 +20,9 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} INIT_TYPE: ${{ inputs.init-type }} + # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not + # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. + SENTRY_URL: http://127.0.0.1:8787 steps: - name: Checkout @@ -72,6 +75,17 @@ jobs: # Lower retention period - we only need this to retry CI. retention-days: 14 + # sentry-cli reporting success is not proof it reached us - it silently falls back to + # sentry.io. Fail loudly when nothing was captured. + - name: Assert debug files were captured + run: | + $dir = "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}/debug-files" + $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) + if ($files.Count -eq 0) { + throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." + } + Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" + - name: Stop envelope capture server if: ${{ always() }} run: | diff --git a/scripts/ci-docker.sh b/scripts/ci-docker.sh index c3d8cc8af..79ccae4da 100755 --- a/scripts/ci-docker.sh +++ b/scripts/ci-docker.sh @@ -44,6 +44,7 @@ docker run -td --name $container \ -e UNITY_VERSION=$unityVersion \ -e GITHUB_ACTIONS="${GITHUB_ACTIONS}" \ -e SENTRY_AUTH_TOKEN="${SENTRY_AUTH_TOKEN:-}" \ + -e SENTRY_URL="${SENTRY_URL:-}" \ --workdir /sentry-unity $image # Generate unique machine-id to avoid any hardcoded values and license-fetch congestion From 50498887d2a6235508bd97f6c72deefeada03fa7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 17 Aug 2026 15:25:33 +0200 Subject: [PATCH 06/18] start capture server inside the build step --- .github/workflows/test-build-android.yml | 13 ++++++------- .github/workflows/test-build-linux.yml | 13 ++++++------- .github/workflows/test-build-macos.yml | 13 ++++++------- .github/workflows/test-build-windows.yml | 13 ++++++------- .github/workflows/test-compile-ios.yml | 9 +++------ .../start-capture-server.ps1 | 11 +++++++++++ 6 files changed, 38 insertions(+), 34 deletions(-) diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 1caef190a..1e1c472f5 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -74,11 +74,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it - # uploads during the build lands in the capture output. See docs/envelope-capture.md. - - name: Start envelope capture server - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; # capture runs don't need that regression check, so compile it out instead of installing it. - name: Disable DependencyConflict @@ -90,7 +85,9 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Export APK - Runtime Initialization - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Runtime) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log @@ -119,7 +116,9 @@ jobs: Set-Content $optionsPath $content - name: Export APK - Build-Time Initialization - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Build-Time) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index b492b4b79..5efef1061 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -85,11 +85,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it - # uploads during the build lands in the capture output. See docs/envelope-capture.md. - - name: Start envelope capture server - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; # capture runs don't need that regression check, so compile it out instead of installing it. - name: Disable DependencyConflict @@ -101,7 +96,9 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Breakpad backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Breakpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log @@ -128,7 +125,9 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Native backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log diff --git a/.github/workflows/test-build-macos.yml b/.github/workflows/test-build-macos.yml index 62532e110..01b832fc7 100644 --- a/.github/workflows/test-build-macos.yml +++ b/.github/workflows/test-build-macos.yml @@ -80,11 +80,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it - # uploads during the build lands in the capture output. See docs/envelope-capture.md. - - name: Start envelope capture server - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; # capture runs don't need that regression check, so compile it out instead of installing it. - name: Disable DependencyConflict @@ -96,7 +91,9 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Cocoa backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -125,7 +122,9 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml index 5f0d18612..cabccb30c 100644 --- a/.github/workflows/test-build-windows.yml +++ b/.github/workflows/test-build-windows.yml @@ -80,11 +80,6 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it - # uploads during the build lands in the capture output. See docs/envelope-capture.md. - - name: Start envelope capture server - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; # capture runs don't need that regression check, so compile it out instead of installing it. - name: Disable DependencyConflict @@ -96,7 +91,9 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Crashpad backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -125,7 +122,9 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" + ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} diff --git a/.github/workflows/test-compile-ios.yml b/.github/workflows/test-compile-ios.yml index e1f09e215..fe33d4a1b 100644 --- a/.github/workflows/test-compile-ios.yml +++ b/.github/workflows/test-compile-ios.yml @@ -36,13 +36,10 @@ jobs: - name: Extract app project run: tar -xvzf "test-app-$env:INIT_TYPE.tar.gz" - # Envelope capture: sentry-cli takes its upload URL from the DSN, so every debug file it - # uploads during the build lands in the capture output. See docs/envelope-capture.md. - - name: Start envelope capture server - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" -Output "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" - - name: iOS test - run: ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" + run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" -Output "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" + ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" timeout-minutes: 20 - name: Assert symbols and sources were uploaded diff --git a/test/Scripts.Integration.Test/start-capture-server.ps1 b/test/Scripts.Integration.Test/start-capture-server.ps1 index 53ceb3e6a..53bbc0d63 100644 --- a/test/Scripts.Integration.Test/start-capture-server.ps1 +++ b/test/Scripts.Integration.Test/start-capture-server.ps1 @@ -17,6 +17,17 @@ if ([string]::IsNullOrEmpty($Output)) { $Output = "test/IntegrationTest/envelopes/$Platform" } +# Build jobs call this from every build step, because a detached server does not reliably survive +# the gap between steps. Reuse the running one instead of fighting over the port and the log file. +try { + Invoke-WebRequest -Uri "http://127.0.0.1:$Port/HEALTH" -TimeoutSec 2 -UseBasicParsing | Out-Null + Write-Host "Envelope capture server already running on port $Port" + exit 0 +} +catch { + # nothing listening yet - start one below +} + $python = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } $server = Join-Path $PSScriptRoot "envelope-capture-server.py" From c6ecc98ed8bb7c08fc6efc77da4ab923f59bb1dc Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Mon, 17 Aug 2026 16:03:48 +0200 Subject: [PATCH 07/18] keep assembled difs ok on re-poll and clear stale port --- .../envelope-capture-server.py | 11 +++++++++++ .../start-capture-server.ps1 | 15 ++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index 8705b48f6..b51d83365 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -38,6 +38,7 @@ chunk_dir = Path(".") symbol_dir = Path(".") platform_name = "unknown" +assembled = set() def parse_envelope(data): @@ -206,6 +207,15 @@ def handle_assemble(self, body): response = {} for checksum, entry in request.items(): name = Path(entry.get("name") or checksum).name + + # sentry-cli polls assemble until every file reports `ok`. Once assembled we drop the + # chunks, so answer from this set rather than re-checking them - otherwise the next + # poll reports the file as missing and sentry-cli fails the upload. + with state_lock: + if checksum in assembled: + response[checksum] = {"state": "ok", "missingChunks": [], "detail": None} + continue + missing = [c for c in entry.get("chunks", []) if not (chunk_dir / c).exists()] if missing: response[checksum] = {"state": "not_found", "missingChunks": missing, "detail": None} @@ -229,6 +239,7 @@ def handle_assemble(self, body): print(f"assembled {target.name} ({target.stat().st_size} bytes)", file=sys.stderr) with state_lock: + assembled.add(checksum) with (symbol_dir / "index.jsonl").open("a") as index: index.write(json.dumps({"file": target.name, "platform": platform_name, "checksum": checksum, "size": target.stat().st_size, diff --git a/test/Scripts.Integration.Test/start-capture-server.ps1 b/test/Scripts.Integration.Test/start-capture-server.ps1 index 53bbc0d63..ae0ac95ba 100644 --- a/test/Scripts.Integration.Test/start-capture-server.ps1 +++ b/test/Scripts.Integration.Test/start-capture-server.ps1 @@ -25,7 +25,20 @@ try { exit 0 } catch { - # nothing listening yet - start one below + # Nothing answered. A server from a previous step may still be holding the port without + # serving (the runner suspends leftovers between steps), which would make the new one fail + # with "Address already in use" - so clear the port before starting. + if ($IsWindows) { + Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } + } + else { + $stale = & lsof -ti "tcp:$Port" 2>$null + foreach ($processId in $stale) { + Write-Host "Killing stale listener on port $Port (pid $processId)" + & kill -9 $processId 2>$null + } + } } $python = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } From aeca1480057da5359fc6ac87e5e9788cd45cdf23 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 09:36:51 +0200 Subject: [PATCH 08/18] keep chunks until shutdown and fix windows rename --- .github/workflows/test-compile-ios.yml | 4 ++++ .../envelope-capture-server.py | 13 +++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-compile-ios.yml b/.github/workflows/test-compile-ios.yml index fe33d4a1b..ce4a51e77 100644 --- a/.github/workflows/test-compile-ios.yml +++ b/.github/workflows/test-compile-ios.yml @@ -23,6 +23,10 @@ jobs: # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. SENTRY_URL: http://127.0.0.1:8787 + # sentry-cli refuses to combine a URL from the environment with the auth token baked into + # sentry.properties ("different configuration source"), so the token has to come from here + # too. The capture server does not check it. + SENTRY_AUTH_TOKEN: envelope-capture-mode steps: - name: Checkout diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index b51d83365..9f4915bc2 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -228,14 +228,15 @@ def handle_assemble(self, body): for chunk in entry["chunks"]: out.write((chunk_dir / chunk).read_bytes()) # Source bundles carry Sentry's "SYSB" magic; mark them so the corpus is self-describing. + # The handle has to be closed before renaming - Windows refuses to rename an open file. with target.open("rb") as probe: - if probe.read(4) == b"SYSB": - target = target.rename(target.with_name(target.name + ".src")) + is_source_bundle = probe.read(4) == b"SYSB" + if is_source_bundle: + target = target.rename(target.with_name(target.name + ".src")) - # IL2CPP debug files run to gigabytes; dropping the chunks once assembled keeps peak - # disk at one copy. sentry-cli re-uploads any chunk a later probe reports missing. - for chunk in entry["chunks"]: - (chunk_dir / chunk).unlink(missing_ok=True) + # Chunks deliberately stay until shutdown: they are deduplicated by hash, so deleting + # them here breaks any other file that shares one and makes sentry-cli fail the upload + # with "Some uploaded files are now missing on the server". print(f"assembled {target.name} ({target.stat().st_size} bytes)", file=sys.stderr) with state_lock: From bd60fb13912ccb3a83ea59228111f41c67d828f4 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 10:37:26 +0200 Subject: [PATCH 09/18] overwrite source bundles on repeat upload --- .../envelope-capture-server.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index 9f4915bc2..5463e506d 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -24,6 +24,7 @@ import sys import tempfile import threading +import traceback import uuid import zlib from datetime import datetime, timezone @@ -232,7 +233,9 @@ def handle_assemble(self, body): with target.open("rb") as probe: is_source_bundle = probe.read(4) == b"SYSB" if is_source_bundle: - target = target.rename(target.with_name(target.name + ".src")) + # replace(), not rename(): a second build re-uploads the same bundles and Windows + # refuses to rename onto an existing file. + target = target.replace(target.with_name(target.name + ".src")) # Chunks deliberately stay until shutdown: they are deduplicated by hash, so deleting # them here breaks any other file that shares one and makes sentry-cli fail the upload @@ -250,6 +253,18 @@ def handle_assemble(self, body): self.respond(200, json.dumps(response).encode()) + def handle_one_request(self): + # An exception escaping a handler closes the connection with no response, which surfaces to + # sentry-cli as "Empty reply from server" and hides the real cause. Answer 500 instead. + try: + super().handle_one_request() + except Exception: + traceback.print_exc() + try: + self.respond(500, json.dumps({"detail": traceback.format_exc()}).encode()) + except Exception: + pass + def do_POST(self): global sequence url = urlparse(self.path) From 66be14f91df475beeccec2cc062f38f45d32d668 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 10:41:37 +0200 Subject: [PATCH 10/18] start capture server inside the test step --- .github/workflows/test-run-android.yml | 9 ++------- .github/workflows/test-run-desktop.yml | 9 +++------ .github/workflows/test-run-ios.yml | 8 +------- .github/workflows/test-run-webgl.yml | 9 +-------- 4 files changed, 7 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test-run-android.yml b/.github/workflows/test-run-android.yml index 176528713..52b8327c1 100644 --- a/.github/workflows/test-run-android.yml +++ b/.github/workflows/test-run-android.yml @@ -84,13 +84,6 @@ jobs: mkdir -p $HOME/.android/avd touch $HOME/.android/repositories.cfg - # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8787. `adb reverse` below tunnels that - # port from the device to the runner. See docs/envelope-capture.md. - - name: Start envelope capture server - if: contains(env.SENTRY_DSN, '127.0.0.1') - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" - - name: Run Android Integration Tests (emulator) if: ${{ steps.device.outputs.device == 'emulator' }} uses: reactivecircus/android-emulator-runner@0a638108440efd5c7f980e6ba145dbcdd8f32009 # v2.37.0 @@ -119,6 +112,7 @@ jobs: adb shell input keyevent 82 adb devices -l adb reverse tcp:8787 tcp:8787 + pwsh -File ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" pwsh -Command '$env:SENTRY_TEST_PLATFORM = "Android"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk"; Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI' # --- arm64 path: Redroid container on the arm64 runner (no hypervisor needed) --- @@ -177,6 +171,7 @@ jobs: } adb devices -l adb reverse tcp:8787 tcp:8787 + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" $env:SENTRY_TEST_PLATFORM = "Android" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index 14b1312a1..d7415309f 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -58,16 +58,11 @@ jobs: run: chmod +x samples/IntegrationTest/Build/test.app/Contents/MacOS/IntegrationTest shell: bash - # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8787. See docs/envelope-capture.md. - - name: Start envelope capture server - if: contains(env.SENTRY_DSN, '127.0.0.1') - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" - - name: Run Integration Tests (Linux) if: inputs.platform == 'linux' timeout-minutes: 20 run: | + pwsh -File ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" xvfb-run pwsh -Command ' $env:SENTRY_TEST_PLATFORM = "Desktop"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test"; @@ -80,6 +75,7 @@ jobs: env: SENTRY_TEST_BACKEND: ${{ inputs.backend }} run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.app/Contents/MacOS/IntegrationTest" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI @@ -88,6 +84,7 @@ jobs: if: inputs.platform == 'windows' timeout-minutes: 20 run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.exe" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI diff --git a/.github/workflows/test-run-ios.yml b/.github/workflows/test-run-ios.yml index 5510181a9..d6e22eeed 100644 --- a/.github/workflows/test-run-ios.yml +++ b/.github/workflows/test-run-ios.yml @@ -65,19 +65,13 @@ jobs: with: xcode-version: '15.0' # to run iOS 17.0 we need Xcode 15.0 - # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8787. The simulator shares the host - # loopback, so no port forwarding is needed. See docs/envelope-capture.md. - - name: Start envelope capture server - if: contains(env.SENTRY_DSN, '127.0.0.1') - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" - - name: Run iOS Integration Tests id: integration-test timeout-minutes: 20 env: SENTRY_IOS_VERSION: ${{ inputs.ios-version }} run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" $env:SENTRY_TEST_PLATFORM = "iOS" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/IntegrationTest.app" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI diff --git a/.github/workflows/test-run-webgl.yml b/.github/workflows/test-run-webgl.yml index f2626708f..bf4b4a1d7 100644 --- a/.github/workflows/test-run-webgl.yml +++ b/.github/workflows/test-run-webgl.yml @@ -43,17 +43,10 @@ jobs: run: pip3 install --upgrade selenium shell: bash - # Envelope capture: only active when ci.yml was dispatched with `capture-envelopes`, which - # points the DSN baked into the test build at 127.0.0.1:8787. The capture server answers CORS - # preflights so the browser can post to it cross-origin, and stays off port 8000 because - # webgl-server.py serves the build there. See docs/envelope-capture.md. - - name: Start envelope capture server - if: contains(env.SENTRY_DSN, '127.0.0.1') - run: ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "webgl-${{ inputs.unity-version }}" - - name: Run Integration Tests timeout-minutes: 20 run: | + ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "webgl-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "WebGL" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build" $env:SENTRY_TEST_UNITY_VERSION = "${{ inputs.unity-version }}" From 3eb3f686b0cceb37cdd766943fcc68d5e8ab3a84 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 12:08:39 +0200 Subject: [PATCH 11/18] document capture wiring --- docs/envelope-capture.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/envelope-capture.md b/docs/envelope-capture.md index c21897253..1fee5f25e 100644 --- a/docs/envelope-capture.md +++ b/docs/envelope-capture.md @@ -10,13 +10,18 @@ can be replayed against a local Sentry instead of going to sentry.io. | [`test/Scripts.Integration.Test/envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for both Sentry endpoints: envelope ingest, and the chunk-upload API sentry-cli uses for debug files. Writes everything to disk. | | [`scripts/replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured envelope corpus to a DSN of your choice. | -**This is a temporary branch change, not a feature.** `ci.yml` hardcodes the DSN to -`http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`. That one switch redirects both -halves of the capture, because the DSN is what decides where each goes: +**This is a temporary branch change, not a feature.** Two settings redirect the two halves: -- the SDK sends envelopes there at **run** time, -- sentry-cli derives its upload URL from that same DSN ([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)), - so debug files land there at **build** time. +- `ci.yml` hardcodes the DSN to `http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`, so + the SDK sends **envelopes** there at run time. +- the build jobs set `SENTRY_URL` to the same address, so sentry-cli uploads **debug files** there + at build time. + +`SENTRY_URL` is needed because sentry-cli 3.x **ignores `defaults.url` in `sentry.properties`**, +which is the only way the SDK knows how to redirect it +([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)). Without it the DSN alone +leaves symbol upload pointed at sentry.io, and the build still reports success - worth fixing +upstream, since it means self-hosted users silently upload their symbols to sentry.io. There is nothing to toggle: push the branch, open the PR, wait for CI, fetch the artifacts. Revert the commit once you have the corpus. @@ -46,10 +51,18 @@ Coverage per matrix entry: `message-capture`, `exception-capture`, `crash-captur emits logs, metrics, sessions and a transaction. Windows/macOS/Linux run twice, once per crash backend (`crashpad`/`breakpad`/`native`/`cocoa`), so the corpus covers each native payload shape. -Two details the capture DSN would otherwise break, both handled: `webgl-server.py` serves the WebGL -build on port 8000, so capture listens on **8787**; and the Linux/Android/WebGL builds run Unity -inside a container, so [`ci-docker.sh`](../scripts/ci-docker.sh) uses `--network host` to let the -in-container sentry-cli reach the capture server. +Details that took a few CI rounds to get right, in case any of them regress: + +- `webgl-server.py` serves the WebGL build on port 8000, so capture listens on **8787**. +- Linux/Android/iOS builds run Unity inside a container, so [`ci-docker.sh`](../scripts/ci-docker.sh) + uses `--network host` and forwards `SENTRY_URL` to let the in-container sentry-cli reach the host. +- The capture server is started **inside** the build/test step that needs it. A detached server does + not survive the gap between steps - the runner leaves it suspended, holding the port without + answering, which shows up as "Empty reply from server". The launcher clears such a leftover first. +- The iOS compile job sets a dummy `SENTRY_AUTH_TOKEN`, because sentry-cli refuses to combine a URL + from the environment with the auth token baked into `sentry.properties`. +- Each build job asserts that debug files actually landed. sentry-cli reporting success is not proof + it reached the capture server - it happily falls back to sentry.io. ### What CI no longer does on this branch From e60a0744a39c5a6e9a92ab856892a08790e3e4d1 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 13:58:36 +0200 Subject: [PATCH 12/18] opt-in capture of envelopes and debug files --- .github/workflows/ci.yml | 177 +++++++++++++----- .github/workflows/test-build-android.yml | 84 +++++---- .github/workflows/test-build-ios.yml | 50 ++++- .github/workflows/test-build-linux.yml | 85 +++++---- .github/workflows/test-build-macos.yml | 85 +++++---- .github/workflows/test-build-windows.yml | 85 +++++---- .github/workflows/test-compile-ios.yml | 95 +++++++--- .github/workflows/test-run-android.yml | 35 ++-- .github/workflows/test-run-desktop.yml | 34 ++-- .github/workflows/test-run-ios.yml | 32 ++-- .github/workflows/test-run-webgl.yml | 32 ++-- docs/envelope-capture.md | 131 +++++-------- scripts/ci-docker.sh | 12 +- scripts/compile-xcode-project.ps1 | 10 + test/IntegrationTest/Integration.Tests.ps1 | 53 ++---- .../Scripts/CliConfiguration.cs | 3 - .../build-project.ps1 | 10 + .../capture-corpus.ps1 | 133 +++++++++++++ .../configure-sentry.ps1 | 7 + .../start-capture-server.ps1 | 66 ------- 20 files changed, 747 insertions(+), 472 deletions(-) create mode 100644 test/Scripts.Integration.Test/capture-corpus.ps1 delete mode 100644 test/Scripts.Integration.Test/start-capture-server.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8633dfaac..bc2392bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,11 @@ on: - "**.md" - "**.txt" workflow_dispatch: # e.g. to manually trigger on foreign PRs + inputs: + capture-corpus: + description: "Capture the raw envelopes and debug files the integration tests produce, as artifacts (see docs/envelope-capture.md). Slower, and the event assertions fail by design." + type: boolean + default: false env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 @@ -73,6 +78,46 @@ jobs: with: unity-version: ${{ matrix.unity-version }} + package-validation: + name: UPM Package validation + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.head_ref || github.ref }} + ssh-key: ${{ secrets.CI_DEPLOY_KEY }} + + - name: Download UPM package + uses: ./.github/actions/wait-for-artifact + with: + name: package-release + + - name: Check snapshot + id: snapshot-check + shell: pwsh + run: | + $ErrorActionPreference = 'Continue' + & ./test/Scripts.Tests/test-pack-contents.ps1 + "result=$LASTEXITCODE" >> $env:GITHUB_OUTPUT + + - name: Update snapshot for dependency bumps + if: steps.snapshot-check.outputs.result != '0' && github.event_name == 'pull_request' && startsWith(github.head_ref, 'deps/') + run: | + ./test/Scripts.Tests/test-pack-contents.ps1 accept + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add test/Scripts.Tests/package-release.zip.snapshot + git commit -m "Update package release snapshot" + git push + + - name: Fail on unexpected snapshot changes + if: steps.snapshot-check.outputs.result != '0' && !(github.event_name == 'pull_request' && startsWith(github.head_ref, 'deps/')) + run: | + Write-Error "Package contents do not match snapshot. Run './test/Scripts.Tests/test-pack-contents.ps1 accept' locally and commit the update." + exit 1 + + # This produces the `samples/IntegrationTest` as `test-${{ matrix.unity-version }}`. test-create: name: Create ${{ matrix.unity-version }} Test Project if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} @@ -102,6 +147,8 @@ jobs: build_platform: WebGL env: UNITY_PATH: docker exec unity unity-editor + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture-corpus && format('test/IntegrationTest/capture/webgl-{0}', matrix.unity-version) || '' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -149,6 +196,26 @@ jobs: restore-keys: | it-library-ubuntu-${{ matrix.unity-version }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-${{ matrix.build_platform }}-${{ matrix.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -BuildDirName "Build-NoSentry" + env: + BUILD_PLATFORM: ${{ matrix.build_platform }} + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-${{ matrix.build_platform }}-${{ matrix.unity-version }} + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -160,16 +227,25 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + if: ${{ !startsWith(matrix.unity-version, '2021') }} + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" + + - name: Disable DependencyConflict (WebGL 2021) + if: ${{ startsWith(matrix.unity-version, '2021') }} run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" env: BUILD_PLATFORM: ${{ matrix.build_platform }} - SENTRY_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build Project run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" @@ -177,6 +253,20 @@ jobs: BUILD_PLATFORM: ${{ matrix.build_platform }} UNITY_VERSION: ${{ matrix.unity-version }} + - name: Compare build sizes + run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform "$env:BUILD_PLATFORM" -UnityVersion "$env:UNITY_VERSION" + env: + BUILD_PLATFORM: ${{ matrix.build_platform }} + UNITY_VERSION: ${{ matrix.unity-version }} + + - name: Upload build size measurement + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-${{ matrix.platform }}-${{ matrix.unity-version }} + path: build-size-measurements/*.json + retention-days: 1 + + # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -211,13 +301,14 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} uses: ./.github/workflows/test-build-android.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-run-android: name: Run Android ${{ matrix.unity-version }} Integration Test @@ -225,12 +316,13 @@ jobs: needs: [test-build-android, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} uses: ./.github/workflows/test-run-android.yml with: unity-version: ${{ matrix.unity-version }} api-level: ${{ matrix.api-level }} init-type: ${{ matrix.init-type }} + capture: ${{ inputs.capture-corpus || false }} strategy: fail-fast: false matrix: @@ -246,13 +338,14 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: ${{ fromJSON(needs.create-unity-matrix.outputs.unity-matrix) }} uses: ./.github/workflows/test-build-ios.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-compile-ios: name: Compile iOS ${{ matrix.unity-version }} Test @@ -267,6 +360,7 @@ jobs: with: unity-version: ${{ matrix.unity-version }} init-type: ${{ matrix.init-type }} + capture: ${{ inputs.capture-corpus || false }} test-run-ios: name: Run iOS ${{ matrix.unity-version }} Integration Test @@ -277,9 +371,10 @@ jobs: unity-version: ${{ matrix.unity-version }} ios-version: ${{ matrix.ios-version }} init-type: ${{ matrix.init-type }} + capture: ${{ inputs.capture-corpus || false }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -303,7 +398,7 @@ jobs: needs: [test-build-webgl, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -311,6 +406,7 @@ jobs: uses: ./.github/workflows/test-run-webgl.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-build-linux: name: Build Linux ${{ matrix.unity-version }} Integration Test @@ -319,7 +415,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -327,6 +423,7 @@ jobs: uses: ./.github/workflows/test-build-linux.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-build-windows: name: Build Windows ${{ matrix.unity-version }} Integration Test @@ -335,7 +432,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -343,6 +440,7 @@ jobs: uses: ./.github/workflows/test-build-windows.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-build-macos: name: Build macOS ${{ matrix.unity-version }} Integration Test @@ -351,7 +449,7 @@ jobs: secrets: UNITY_LICENSE_SERVER_CONFIG: ${{ secrets.UNITY_LICENSE_SERVER_CONFIG }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -359,6 +457,7 @@ jobs: uses: ./.github/workflows/test-build-macos.yml with: unity-version: ${{ matrix.unity-version }} + capture: ${{ inputs.capture-corpus || false }} test-run-linux: name: Run Linux ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -366,7 +465,7 @@ jobs: needs: [test-build-linux, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -377,6 +476,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: linux backend: ${{ matrix.backend }} + capture: ${{ inputs.capture-corpus || false }} test-run-windows: name: Run Windows ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -384,7 +484,7 @@ jobs: needs: [test-build-windows, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -395,6 +495,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: windows backend: ${{ matrix.backend }} + capture: ${{ inputs.capture-corpus || false }} test-run-macos: name: Run macOS ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -402,7 +503,7 @@ jobs: needs: [test-build-macos, create-unity-matrix] secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_TEST_DSN: http://capture@127.0.0.1:8787/1 # TEMPORARY: envelope capture, see docs/envelope-capture.md + SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} strategy: fail-fast: false matrix: @@ -413,41 +514,23 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: macos backend: ${{ matrix.backend }} + capture: ${{ inputs.capture-corpus || false }} - # Merges the per-job envelope artifacts into a single `envelopes-all` download. - # See docs/envelope-capture.md. - collect-envelopes: - name: Collect captured envelopes + build-size-summary: + name: Build Size runs-on: ubuntu-latest - if: ${{ always() }} - needs: [test-run-android, test-run-ios, test-run-webgl, test-run-linux, test-run-windows, test-run-macos] + if: ${{ !startsWith(github.ref, 'refs/heads/release/') }} + needs: [test-build-webgl, test-build-android, test-compile-ios, test-build-linux, test-build-windows, test-build-macos] steps: - - name: Download all envelope artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: envelopes-* - merge-multiple: true - path: envelopes + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Summarize corpus - run: | - "## Captured envelopes`n" >> $env:GITHUB_STEP_SUMMARY - "| Platform | Payloads | Test actions |" >> $env:GITHUB_STEP_SUMMARY - "| --- | --- | --- |" >> $env:GITHUB_STEP_SUMMARY - $total = 0 - Get-ChildItem -Path envelopes -Recurse -Filter index.jsonl | Sort-Object { $_.Directory.Name } | ForEach-Object { - $entries = Get-Content $_.FullName | ForEach-Object { $_ | ConvertFrom-Json } - $total += $entries.Count - $actions = ($entries | ForEach-Object { $_.label } | Sort-Object -Unique) -join ", " - "| $($_.Directory.Name) | $($entries.Count) | $actions |" >> $env:GITHUB_STEP_SUMMARY - } - "`nTotal: $total payloads" >> $env:GITHUB_STEP_SUMMARY - - - name: Upload combined corpus - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + - name: Download all build size measurements + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: envelopes-all - path: envelopes/ - if-no-files-found: error - retention-days: 14 + pattern: build-size-* + path: build-size-measurements + - name: Create consolidated summary + shell: pwsh + run: ./scripts/create-build-size-summary.ps1 \ No newline at end of file diff --git a/.github/workflows/test-build-android.yml b/.github/workflows/test-build-android.yml index 1e1c472f5..d15a853b1 100644 --- a/.github/workflows/test-build-android.yml +++ b/.github/workflows/test-build-android.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: UNITY_LICENSE_SERVER_CONFIG: required: true @@ -25,9 +30,8 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} - # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not - # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. - SENTRY_URL: http://127.0.0.1:8787 + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/android-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -63,6 +67,24 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Android-${{ inputs.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -BuildDirName "Build-NoSentry" + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Android-${{ inputs.unity-version }} + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -74,10 +96,14 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" @@ -85,13 +111,21 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Export APK - Runtime Initialization - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Runtime) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log + - name: Compare build sizes (Runtime) + run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + + - name: Upload build size measurement + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-Android-${{ env.UNITY_VERSION }} + path: build-size-measurements/*.json + retention-days: 1 + - name: Bundle build & symbol-upload logs with the APK (Runtime) run: | Copy-Item unity.log samples/IntegrationTest/Build/ -ErrorAction SilentlyContinue @@ -116,9 +150,7 @@ jobs: Set-Content $optionsPath $content - name: Export APK - Build-Time Initialization - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "Android" -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Build-Time) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath samples/IntegrationTest/Logs/sentry-symbols-upload.log @@ -151,29 +183,11 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 # Lower retention period - we only need this to retry CI. - # sentry-cli reporting success is not proof it reached us - it silently falls back to - # sentry.io. Fail loudly when nothing was captured. - - name: Assert debug files were captured - run: | - $dir = "test/IntegrationTest/symbols/android-${{ env.UNITY_VERSION }}/debug-files" - $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) - if ($files.Count -eq 0) { - throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." - } - Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" - - - name: Stop envelope capture server - if: ${{ always() }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured debug files - if: ${{ always() }} + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: symbols-android-${{ env.UNITY_VERSION }} - path: test/IntegrationTest/symbols/ + name: corpus-android-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ if-no-files-found: warn retention-days: 14 - diff --git a/.github/workflows/test-build-ios.yml b/.github/workflows/test-build-ios.yml index 7fe7aa7fe..729eb7784 100644 --- a/.github/workflows/test-build-ios.yml +++ b/.github/workflows/test-build-ios.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: UNITY_LICENSE_SERVER_CONFIG: required: true @@ -31,6 +36,8 @@ jobs: GITHUB_ACTOR: ${{ github.actor }} UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/ios-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -66,6 +73,37 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-iOS-${{ inputs.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform "iOS" -BuildDirName "Build-NoSentry" + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-iOS-${{ inputs.unity-version }} + + - name: Create archive for build without Sentry + shell: bash + run: | + rm -rf samples/IntegrationTest/Build-NoSentry/*_BackUpThisFolder_ButDontShipItWithYourGame + tar -cvzf test-app-no-sentry.tar.gz samples/IntegrationTest/Build-NoSentry + + - name: Upload build without Sentry + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: testapp-ios-no-sentry-${{ env.UNITY_VERSION }} + path: test-app-no-sentry.tar.gz + retention-days: 1 + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -77,10 +115,14 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform "$env:BUILD_PLATFORM" diff --git a/.github/workflows/test-build-linux.yml b/.github/workflows/test-build-linux.yml index 5efef1061..1065348c9 100644 --- a/.github/workflows/test-build-linux.yml +++ b/.github/workflows/test-build-linux.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: UNITY_LICENSE_SERVER_CONFIG: required: true @@ -26,9 +31,8 @@ jobs: UNITY_PATH: docker exec unity unity-editor UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Linux - # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not - # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. - SENTRY_URL: http://127.0.0.1:8787 + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/linux-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -74,6 +78,24 @@ jobs: restore-keys: | it-library-ubuntu-${{ env.UNITY_VERSION }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Linux-${{ inputs.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -BuildDirName "Build-NoSentry" + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Linux-${{ inputs.unity-version }} + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -85,10 +107,14 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux @@ -96,13 +122,22 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Breakpad backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Breakpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log + - name: Compare build sizes + run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + + - name: Upload build size measurement + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-Linux-${{ env.UNITY_VERSION }} + path: build-size-measurements/*.json + retention-days: 1 + + # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Breakpad backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -125,9 +160,7 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "linux-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Linux -UnityVersion "$env:UNITY_VERSION" - name: Assert symbols and sources were uploaded (Native backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log @@ -178,29 +211,11 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 - # sentry-cli reporting success is not proof it reached us - it silently falls back to - # sentry.io. Fail loudly when nothing was captured. - - name: Assert debug files were captured - run: | - $dir = "test/IntegrationTest/symbols/linux-${{ env.UNITY_VERSION }}/debug-files" - $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) - if ($files.Count -eq 0) { - throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." - } - Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" - - - name: Stop envelope capture server - if: ${{ always() }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured debug files - if: ${{ always() }} + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: symbols-linux-${{ env.UNITY_VERSION }} - path: test/IntegrationTest/symbols/ + name: corpus-linux-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ if-no-files-found: warn retention-days: 14 - diff --git a/.github/workflows/test-build-macos.yml b/.github/workflows/test-build-macos.yml index 01b832fc7..e06b567f8 100644 --- a/.github/workflows/test-build-macos.yml +++ b/.github/workflows/test-build-macos.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: UNITY_LICENSE_SERVER_CONFIG: required: true @@ -24,9 +29,8 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: MacOS - # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not - # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. - SENTRY_URL: http://127.0.0.1:8787 + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/macos-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -69,6 +73,24 @@ jobs: restore-keys: | it-library-macos-${{ env.UNITY_VERSION }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-MacOS-${{ inputs.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -BuildDirName "Build-NoSentry" + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-MacOS-${{ inputs.unity-version }} + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -80,10 +102,14 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS @@ -91,15 +117,24 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Cocoa backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Cocoa backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log + - name: Compare build sizes + run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" + + - name: Upload build size measurement + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-MacOS-${{ env.UNITY_VERSION }} + path: build-size-measurements/*.json + retention-days: 1 + + # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Cocoa backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -122,9 +157,7 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "macos-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform MacOS -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -164,29 +197,11 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 - # sentry-cli reporting success is not proof it reached us - it silently falls back to - # sentry.io. Fail loudly when nothing was captured. - - name: Assert debug files were captured - run: | - $dir = "test/IntegrationTest/symbols/macos-${{ env.UNITY_VERSION }}/debug-files" - $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) - if ($files.Count -eq 0) { - throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." - } - Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" - - - name: Stop envelope capture server - if: ${{ always() }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured debug files - if: ${{ always() }} + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: symbols-macos-${{ env.UNITY_VERSION }} - path: test/IntegrationTest/symbols/ + name: corpus-macos-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ if-no-files-found: warn retention-days: 14 - diff --git a/.github/workflows/test-build-windows.yml b/.github/workflows/test-build-windows.yml index cabccb30c..f13ac179e 100644 --- a/.github/workflows/test-build-windows.yml +++ b/.github/workflows/test-build-windows.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: UNITY_LICENSE_SERVER_CONFIG: required: true @@ -24,9 +29,8 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} BUILD_PLATFORM: Windows - # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not - # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. - SENTRY_URL: http://127.0.0.1:8787 + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/windows-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -69,6 +73,24 @@ jobs: restore-keys: | it-library-windows-${{ env.UNITY_VERSION }}- + - name: Restore cached build without Sentry + id: cache-build-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Windows-${{ inputs.unity-version }} + + - name: Build without Sentry SDK + if: steps.cache-build-nosentry.outputs.cache-hit != 'true' + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -BuildDirName "Build-NoSentry" + + - name: Save cached build without Sentry + if: github.ref == 'refs/heads/main' && steps.cache-build-nosentry.outputs.cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: samples/IntegrationTest/Build-NoSentry + key: build-nosentry-Windows-${{ inputs.unity-version }} + - name: Download UPM package uses: ./.github/actions/wait-for-artifact with: @@ -80,10 +102,14 @@ jobs: - name: Add Sentry to the project run: ./test/Scripts.Integration.Test/add-sentry.ps1 -UnityPath "$env:UNITY_PATH" -PackagePath "test-package-release" - # IntegrationTester.cs calls into the DependencyConflict package unless this define is set; - # capture runs don't need that regression check, so compile it out instead of installing it. - - name: Disable DependencyConflict - run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -Disable + - name: Download DependencyConflict package + uses: ./.github/actions/wait-for-artifact + with: + name: dependency-conflict-package + path: dependency-conflict-package + + - name: Add DependencyConflict to the project + run: ./test/Scripts.Integration.Test/add-dependency-conflict.ps1 -PackagePath "dependency-conflict-package" - name: Configure Sentry run: ./test/Scripts.Integration.Test/configure-sentry.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows @@ -91,15 +117,24 @@ jobs: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} - name: Build with Sentry SDK (Crashpad backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - name: Assert symbols and sources were uploaded (Crashpad backend) run: ./test/Scripts.Integration.Test/assert-symbol-upload.ps1 -LogPath unity.log + - name: Compare build sizes + run: ./test/Scripts.Integration.Test/measure-build-size.ps1 -Path1 "samples/IntegrationTest/Build-NoSentry" -Path2 "samples/IntegrationTest/Build" -Platform Windows -UnityVersion "$env:UNITY_VERSION" + + - name: Upload build size measurement + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-Windows-${{ env.UNITY_VERSION }} + path: build-size-measurements/*.json + retention-days: 1 + + # We create tar explicitly because upload-artifact is slow for many files. - name: Create archive (Crashpad backend) run: | Remove-Item -Recurse -Force samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame -ErrorAction SilentlyContinue @@ -122,9 +157,7 @@ jobs: Set-Content $optionsPath $content - name: Build with Sentry SDK (Native backend) - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "windows-${{ env.UNITY_VERSION }}" -Output "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}" - ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" + run: ./test/Scripts.Integration.Test/build-project.ps1 -UnityPath "$env:UNITY_PATH" -Platform Windows -UnityVersion "$env:UNITY_VERSION" env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} @@ -164,29 +197,11 @@ jobs: !samples/IntegrationTest/Build/*_BackUpThisFolder_ButDontShipItWithYourGame retention-days: 14 - # sentry-cli reporting success is not proof it reached us - it silently falls back to - # sentry.io. Fail loudly when nothing was captured. - - name: Assert debug files were captured - run: | - $dir = "test/IntegrationTest/symbols/windows-${{ env.UNITY_VERSION }}/debug-files" - $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) - if ($files.Count -eq 0) { - throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." - } - Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" - - - name: Stop envelope capture server - if: ${{ always() }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured debug files - if: ${{ always() }} + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: symbols-windows-${{ env.UNITY_VERSION }} - path: test/IntegrationTest/symbols/ + name: corpus-windows-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ if-no-files-found: warn retention-days: 14 - diff --git a/.github/workflows/test-compile-ios.yml b/.github/workflows/test-compile-ios.yml index ce4a51e77..5b29d6091 100644 --- a/.github/workflows/test-compile-ios.yml +++ b/.github/workflows/test-compile-ios.yml @@ -8,6 +8,11 @@ on: init-type: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" defaults: run: @@ -20,18 +25,52 @@ jobs: env: UNITY_VERSION: ${{ inputs.unity-version }} INIT_TYPE: ${{ inputs.init-type }} - # sentry-cli 3.x ignores `defaults.url` from sentry.properties, so the DSN alone does not - # redirect symbol upload. SENTRY_URL is what it honours. See docs/envelope-capture.md. - SENTRY_URL: http://127.0.0.1:8787 - # sentry-cli refuses to combine a URL from the environment with the auth token baked into - # sentry.properties ("different configuration source"), so the token has to come from here - # too. The capture server does not check it. - SENTRY_AUTH_TOKEN: envelope-capture-mode + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/ios-{0}-{1}', inputs.unity-version, inputs.init-type) || '' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Restore cached compiled iOS build without Sentry + if: ${{ inputs.init-type == 'runtime' }} + id: cache-compiled-nosentry + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: IntegrationTest-NoSentry.app + key: build-nosentry-iOS-compiled-${{ inputs.unity-version }} + + - name: Download build without Sentry (for size comparison) + if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: testapp-ios-no-sentry-${{ env.UNITY_VERSION }} + + - name: Extract and compile build without Sentry + if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} + run: | + tar -xvzf test-app-no-sentry.tar.gz + Move-Item -Path "samples/IntegrationTest/Build-NoSentry" -Destination "samples/IntegrationTest/Build" + + - name: Compile Xcode project without Sentry + if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} + run: ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" + timeout-minutes: 20 + + - name: Save compiled app without Sentry + if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' }} + run: | + Copy-Item -Path "samples/IntegrationTest/Build/archive/Unity-iPhone/Build/Products/Release-iphonesimulator/IntegrationTest.app" ` + -Destination "IntegrationTest-NoSentry.app" -Recurse + Remove-Item -Path "samples/IntegrationTest/Build" -Recurse -Force + + - name: Save cached compiled iOS build without Sentry + if: ${{ inputs.init-type == 'runtime' && steps.cache-compiled-nosentry.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main' }} + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: IntegrationTest-NoSentry.app + key: build-nosentry-iOS-compiled-${{ inputs.unity-version }} + - name: Download app project uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -41,9 +80,7 @@ jobs: run: tar -xvzf "test-app-$env:INIT_TYPE.tar.gz" - name: iOS test - run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" -Output "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" - ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" + run: ./scripts/compile-xcode-project.ps1 -iOSMinVersion "17.0" timeout-minutes: 20 - name: Assert symbols and sources were uploaded @@ -76,28 +113,28 @@ jobs: # Lower retention period - we only need this to retry CI. retention-days: 14 - # sentry-cli reporting success is not proof it reached us - it silently falls back to - # sentry.io. Fail loudly when nothing was captured. - - name: Assert debug files were captured + - name: Compare build sizes + if: ${{ inputs.init-type == 'runtime' }} run: | - $dir = "test/IntegrationTest/symbols/ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}/debug-files" - $files = @(Get-ChildItem $dir -File -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne "index.jsonl" }) - if ($files.Count -eq 0) { - throw "No debug files captured in '$dir' - sentry-cli did not upload to the capture server." - } - Write-Host "Captured $($files.Count) debug files ($([math]::Round(($files | Measure-Object Length -Sum).Sum / 1MB, 1)) MB)" - - - name: Stop envelope capture server - if: ${{ always() }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } + ./test/Scripts.Integration.Test/measure-build-size.ps1 ` + -Path1 "IntegrationTest-NoSentry.app" ` + -Path2 "samples/IntegrationTest/Build/archive/Unity-iPhone/Build/Products/Release-iphonesimulator/IntegrationTest.app" ` + -Platform "iOS" ` + -UnityVersion "$env:UNITY_VERSION" + + - name: Upload build size measurement + if: ${{ inputs.init-type == 'runtime' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-size-iOS-${{ env.UNITY_VERSION }} + path: build-size-measurements/*.json + retention-days: 1 - - name: Upload captured debug files - if: ${{ always() }} + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: symbols-ios-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }} - path: test/IntegrationTest/symbols/ + name: corpus-ios-${{ inputs.unity-version }}-${{ inputs.init-type }} + path: test/IntegrationTest/capture/ if-no-files-found: warn retention-days: 14 diff --git a/.github/workflows/test-run-android.yml b/.github/workflows/test-run-android.yml index 52b8327c1..6e1cf2ece 100644 --- a/.github/workflows/test-run-android.yml +++ b/.github/workflows/test-run-android.yml @@ -11,6 +11,11 @@ on: init-type: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: SENTRY_AUTH_TOKEN: required: true @@ -32,6 +37,8 @@ jobs: HOMEBREW_NO_INSTALL_CLEANUP: 1 SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/run-android-{0}-{1}-{2}', inputs.unity-version, inputs.api-level, inputs.init-type) || '' }} steps: - name: Checkout @@ -111,8 +118,6 @@ jobs: adb wait-for-device adb shell input keyevent 82 adb devices -l - adb reverse tcp:8787 tcp:8787 - pwsh -File ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" pwsh -Command '$env:SENTRY_TEST_PLATFORM = "Android"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk"; Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI' # --- arm64 path: Redroid container on the arm64 runner (no hypervisor needed) --- @@ -170,27 +175,10 @@ jobs: Start-Sleep -Seconds 5 } adb devices -l - adb reverse tcp:8787 tcp:8787 - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }}" $env:SENTRY_TEST_PLATFORM = "Android" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.apk" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI - - name: Stop envelope capture server - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured envelopes - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: envelopes-android-${{ inputs.api-level }}-${{ inputs.unity-version }}-${{ inputs.init-type }} - path: test/IntegrationTest/envelopes/ - if-no-files-found: warn - retention-days: 14 - - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -200,3 +188,12 @@ jobs: ${{ env.ARTIFACTS_PATH }} test/IntegrationTest/results/ retention-days: 14 + + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: corpus-run-android-${{ inputs.unity-version }}-${{ inputs.api-level }}-${{ inputs.init-type }} + path: test/IntegrationTest/capture/ + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/test-run-desktop.yml b/.github/workflows/test-run-desktop.yml index d7415309f..f08ebea64 100644 --- a/.github/workflows/test-run-desktop.yml +++ b/.github/workflows/test-run-desktop.yml @@ -14,6 +14,11 @@ on: type: string default: "" description: "macOS: native or cocoa. Windows: native or crashpad. Linux: native or breakpad." + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: SENTRY_AUTH_TOKEN: required: true @@ -31,6 +36,8 @@ jobs: env: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/run-{0}{1}-{2}', inputs.platform, inputs.backend && format('-{0}', inputs.backend) || '', inputs.unity-version) || '' }} steps: - name: Checkout @@ -62,7 +69,6 @@ jobs: if: inputs.platform == 'linux' timeout-minutes: 20 run: | - pwsh -File ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" xvfb-run pwsh -Command ' $env:SENTRY_TEST_PLATFORM = "Desktop"; $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test"; @@ -75,7 +81,6 @@ jobs: env: SENTRY_TEST_BACKEND: ${{ inputs.backend }} run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.app/Contents/MacOS/IntegrationTest" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI @@ -84,26 +89,10 @@ jobs: if: inputs.platform == 'windows' timeout-minutes: 20 run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "Desktop" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/test.exe" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI - - name: Stop envelope capture server - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured envelopes - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: envelopes-${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }} - path: test/IntegrationTest/envelopes/ - if-no-files-found: warn - retention-days: 14 - - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -112,3 +101,12 @@ jobs: path: | test/IntegrationTest/results/ retention-days: 14 + + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: corpus-run-${{ inputs.platform }}${{ inputs.backend && format('-{0}', inputs.backend) || '' }}-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/test-run-ios.yml b/.github/workflows/test-run-ios.yml index d6e22eeed..1f6577cbe 100644 --- a/.github/workflows/test-run-ios.yml +++ b/.github/workflows/test-run-ios.yml @@ -11,6 +11,11 @@ on: init-type: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" # Map the workflow outputs to job outputs outputs: status: @@ -40,6 +45,8 @@ jobs: ARTIFACTS_PATH: samples/IntegrationTest/test-artifacts/ SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/run-ios-{0}-{1}-{2}', inputs.unity-version, inputs.ios-version, inputs.init-type) || '' }} steps: - name: Checkout @@ -71,27 +78,11 @@ jobs: env: SENTRY_IOS_VERSION: ${{ inputs.ios-version }} run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }}" $env:SENTRY_TEST_PLATFORM = "iOS" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build/IntegrationTest.app" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI echo "status=success" >> $env:GITHUB_OUTPUT - - name: Stop envelope capture server - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured envelopes - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: envelopes-ios-${{ env.IOS_VERSION }}-${{ env.UNITY_VERSION }}-${{ env.INIT_TYPE }} - path: test/IntegrationTest/envelopes/ - if-no-files-found: warn - retention-days: 14 - - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -101,3 +92,12 @@ jobs: ${{ env.ARTIFACTS_PATH }} test/IntegrationTest/results/ retention-days: 14 + + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: corpus-run-ios-${{ inputs.unity-version }}-${{ inputs.ios-version }}-${{ inputs.init-type }} + path: test/IntegrationTest/capture/ + if-no-files-found: warn + retention-days: 14 diff --git a/.github/workflows/test-run-webgl.yml b/.github/workflows/test-run-webgl.yml index bf4b4a1d7..38c0fd204 100644 --- a/.github/workflows/test-run-webgl.yml +++ b/.github/workflows/test-run-webgl.yml @@ -5,6 +5,11 @@ on: unity-version: required: true type: string + capture: + required: false + type: boolean + default: false + description: "Capture the envelopes and debug files this job produces - see docs/envelope-capture.md" secrets: SENTRY_AUTH_TOKEN: required: true @@ -22,6 +27,8 @@ jobs: env: SENTRY_DSN: ${{ secrets.SENTRY_TEST_DSN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + # Empty unless a capture run was dispatched; see docs/envelope-capture.md. + SENTRY_CAPTURE_PATH: ${{ inputs.capture && format('test/IntegrationTest/capture/run-webgl-{0}', inputs.unity-version) || '' }} steps: - name: Checkout @@ -46,27 +53,11 @@ jobs: - name: Run Integration Tests timeout-minutes: 20 run: | - ./test/Scripts.Integration.Test/start-capture-server.ps1 -Platform "webgl-${{ inputs.unity-version }}" $env:SENTRY_TEST_PLATFORM = "WebGL" $env:SENTRY_TEST_APP = "samples/IntegrationTest/Build" $env:SENTRY_TEST_UNITY_VERSION = "${{ inputs.unity-version }}" Invoke-Pester -Path test/IntegrationTest/Integration.Tests.ps1 -CI - - name: Stop envelope capture server - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - run: | - try { Invoke-WebRequest -Uri "http://127.0.0.1:8787/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null } - catch { Write-Host "Capture server already gone" } - - - name: Upload captured envelopes - if: ${{ always() && contains(env.SENTRY_DSN, '127.0.0.1') }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: envelopes-webgl-${{ inputs.unity-version }} - path: test/IntegrationTest/envelopes/ - if-no-files-found: warn - retention-days: 14 - - name: Upload test results on failure if: ${{ failure() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -75,3 +66,12 @@ jobs: path: | test/IntegrationTest/results/ retention-days: 14 + + - name: Upload captured corpus + if: ${{ always() && inputs.capture }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: corpus-run-webgl-${{ inputs.unity-version }} + path: test/IntegrationTest/capture/ + if-no-files-found: warn + retention-days: 14 diff --git a/docs/envelope-capture.md b/docs/envelope-capture.md index 1fee5f25e..a742f4e6c 100644 --- a/docs/envelope-capture.md +++ b/docs/envelope-capture.md @@ -1,92 +1,39 @@ -# Capturing a raw envelope + debug file corpus from the integration tests +# Capturing an envelope + debug file corpus The integration tests exercise every managed error, native crash and app hang path the SDK has, on every platform we ship. Capture mode records what those runs actually put on the wire - raw -envelopes, minidump uploads, and the debug files sentry-cli uploads at build time - so the whole lot -can be replayed against a local Sentry instead of going to sentry.io. +envelopes, minidump uploads, and the debug files sentry-cli uploads - so the whole lot can be +replayed against a local Sentry to work on event processing or symbolication. -| | | -|---|---| -| [`test/Scripts.Integration.Test/envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for both Sentry endpoints: envelope ingest, and the chunk-upload API sentry-cli uses for debug files. Writes everything to disk. | -| [`scripts/replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured envelope corpus to a DSN of your choice. | - -**This is a temporary branch change, not a feature.** Two settings redirect the two halves: - -- `ci.yml` hardcodes the DSN to `http://capture@127.0.0.1:8787/1` in place of `SENTRY_TEST_DSN`, so - the SDK sends **envelopes** there at run time. -- the build jobs set `SENTRY_URL` to the same address, so sentry-cli uploads **debug files** there - at build time. +It is **off by default** and changes nothing about a normal CI run. -`SENTRY_URL` is needed because sentry-cli 3.x **ignores `defaults.url` in `sentry.properties`**, -which is the only way the SDK knows how to redirect it -([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)). Without it the DSN alone -leaves symbol upload pointed at sentry.io, and the build still reports success - worth fixing -upstream, since it means self-hosted users silently upload their symbols to sentry.io. +## Running it -There is nothing to toggle: push the branch, open the PR, wait for CI, fetch the artifacts. Revert -the commit once you have the corpus. +Actions → **CI** → *Run workflow* → tick **capture-corpus**. -## What CI produces - -| Artifact | From | Contents | -|---|---|---| -| `envelopes-all` | run jobs, merged | every envelope and minidump upload, one directory per platform | -| `symbols--` | build jobs | the debug files and source bundles sentry-cli uploaded for that build | +Every build and run job then writes its corpus to an artifact: ```bash -gh run download -n envelopes-all -D ./corpus -gh run download -p 'symbols-*' -D ./corpus/symbols +gh run download -p 'corpus-*' -D ./corpus ``` -Debug files are big (IL2CPP `GameAssembly.pdb` and friends), so they stay per-platform rather than -being merged into one download. +| Artifact | Contains | +|---|---| +| `corpus--` | debug files and source bundles sentry-cli uploaded for that build | +| `corpus-run---` | the envelopes and minidump uploads that run produced | + +Debug files are large (IL2CPP `GameAssembly.pdb` and friends run to hundreds of MB per platform), so +they stay per-job rather than being merged into one download. -**The integration tests fail by design in capture mode.** There is no backend to verify against, so +**The event assertions fail by design in a capture run.** There is no backend to verify against, so `Integration.Tests.ps1` skips the Sentry API lookups and every event assertion fails. The artifacts -are the deliverable; a red run is expected. The symbol-upload assertions are the exception and still -mean something: they pass only if sentry-cli really did upload debug files to the capture server. +are the deliverable; a red run is expected. Coverage per matrix entry: `message-capture`, `exception-capture`, `crash-capture` (+ the `crash-send` relaunch that flushes the crash envelope) and `app-hang-capture`, each of which also emits logs, metrics, sessions and a transaction. Windows/macOS/Linux run twice, once per crash backend (`crashpad`/`breakpad`/`native`/`cocoa`), so the corpus covers each native payload shape. -Details that took a few CI rounds to get right, in case any of them regress: - -- `webgl-server.py` serves the WebGL build on port 8000, so capture listens on **8787**. -- Linux/Android/iOS builds run Unity inside a container, so [`ci-docker.sh`](../scripts/ci-docker.sh) - uses `--network host` and forwards `SENTRY_URL` to let the in-container sentry-cli reach the host. -- The capture server is started **inside** the build/test step that needs it. A detached server does - not survive the gap between steps - the runner leaves it suspended, holding the port without - answering, which shows up as "Empty reply from server". The launcher clears such a leftover first. -- The iOS compile job sets a dummy `SENTRY_AUTH_TOKEN`, because sentry-cli refuses to combine a URL - from the environment with the auth token baked into `sentry.properties`. -- Each build job asserts that debug files actually landed. sentry-cli reporting success is not proof - it reached the capture server - it happily falls back to sentry.io. - -### What CI no longer does on this branch - -Stripped to keep the run short and the failures meaningful: the UPM package snapshot validation, all -build-size measurement (including every "build without Sentry" pass and the `build-size-summary` -job), and the dependency-conflict package steps. - -## What lands on disk - -One directory per matrix entry, so the merged corpus stays collision-free: - -``` -macos-cocoa-6000.5/001-macos-cocoa-6000.5-exception-capture-event_attachment.envelope # raw bytes, gunzipped -macos-cocoa-6000.5/001-macos-cocoa-6000.5-exception-capture-event_attachment.meta.json # path, headers, item types -windows-crashpad-6000.5/003-...-crash-capture-minidump.multipart.bin # crashpad minidump upload -windows-crashpad-6000.5/index.jsonl # one line per request -symbols/macos-6000.5/debug-files/--GameAssembly.dylib # debug companion -symbols/macos-6000.5/debug-files/--GameAssembly.dylib.src # source bundle -symbols/macos-6000.5/debug-files/index.jsonl # debug id -> file -``` - -A dif and its source bundle share a debug id *and* a name, so the checksum in the file name is what -keeps them apart. - ## Replaying into a local Sentry ```bash @@ -95,12 +42,9 @@ python3 scripts/replay-envelopes.py ./corpus --dsn http://@localhost:9000/1 # the debug files that symbolicate them sentry-cli --url http://localhost:9000 --auth-token debug-files upload \ - -o -p ./corpus/symbols/macos-6000.5/debug-files + -o -p ./corpus/corpus-macos-6000.5/macos-6000.5/debug-files ``` -sentry-cli reads the captured files straight out of the artifact and re-uploads them under their -original debug ids, which is what lets the replayed crashes symbolicate. - Each envelope is rewritten before it is posted: the DSN in the envelope header is swapped for the target, `sent_at` is set to now, event ids are regenerated and all timestamps are shifted to now while keeping their relative offsets (breadcrumbs, spans, session start). That keeps a corpus @@ -110,14 +54,43 @@ Pass `--keep-ids` / `--keep-timestamps` to replay the bytes as they were capture Minidump uploads are replayed verbatim to `/api//minidump/` with only the ingest key swapped - the event ids inside the multipart body are left alone. +## How it works + +Everything keys off one environment variable, **`SENTRY_CAPTURE_PATH`**. The workflows set it from +the `capture` input; unset, every hook below is a no-op. + +| Piece | Role | +|---|---| +| [`capture-corpus.ps1`](../test/Scripts.Integration.Test/capture-corpus.ps1) | `Test-CaptureEnabled` / `Start-CaptureServer` / `Set-CaptureLabel`, and the capture DSN and port | +| [`envelope-capture-server.py`](../test/Scripts.Integration.Test/envelope-capture-server.py) | Stands in for both Sentry endpoints: envelope ingest, and the chunk-upload API sentry-cli uses for debug files | +| [`replay-envelopes.py`](../scripts/replay-envelopes.py) | Posts a captured corpus to a DSN of your choice | +| `configure-sentry.ps1` | Bakes the capture DSN into the test app | +| `build-project.ps1`, `compile-xcode-project.ps1` | Start the server and point sentry-cli at it for the build | +| `Integration.Tests.ps1` | Starts the server for the test run, labels each action, skips API verification | +| `ci-docker.sh` | In capture mode only, shares the host network so the in-container sentry-cli can reach the server | + +Details worth knowing if any of this regresses: + +- The server is started **inside** the build or test step that needs it, never in a step of its own: + a server started earlier does not survive the gap - the runner leaves it suspended, holding the + port without answering, which surfaces as "Empty reply from server". `Start-CaptureServer` clears + such a leftover before binding, and is safe to call repeatedly within a job. +- `SENTRY_URL` is what redirects sentry-cli. **sentry-cli 3.x ignores `defaults.url` in + `sentry.properties`**, which is the only knob the SDK offers + ([`SentryCli.UrlOverride`](../src/Sentry.Unity.Editor/SentryCli.cs)) - so without it, symbol upload + silently goes to sentry.io and the build still reports success. That also means self-hosted users + currently upload their symbols to sentry.io; worth fixing upstream. +- The iOS Xcode phase additionally needs `SENTRY_AUTH_TOKEN` from the environment, because sentry-cli + refuses to combine a URL from the environment with a token from `sentry.properties`. +- Capture listens on **8787**; `webgl-server.py` already serves the WebGL build on 8000. +- macOS ATS blocks plain HTTP to an IP literal, so the test app's `Info.plist` gets + `NSAllowsArbitraryLoads` ([`AllowInsecureHttp.cs`](../test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs)). + ## Capturing locally ```bash -python3 test/Scripts.Integration.Test/envelope-capture-server.py --output ./out --platform macos -SENTRY_DSN="http://capture@127.0.0.1:8787/1" \ +SENTRY_CAPTURE_PATH=$PWD/corpus/macos \ ./test/Scripts.Integration.Test/dev-integration-test.ps1 -UnityVersion 6000.5 -Platform MacOS ``` -Any DSN whose host is `127.0.0.1`, `localhost` or `10.0.2.2` puts `Integration.Tests.ps1` into -capture mode. Note that this also applies when you point the tests straight at a locally running -Sentry - the run works, but the API verification is skipped. +The same hooks apply, so a local run produces the same corpus layout as CI. diff --git a/scripts/ci-docker.sh b/scripts/ci-docker.sh index 79ccae4da..ebbeb511c 100755 --- a/scripts/ci-docker.sh +++ b/scripts/ci-docker.sh @@ -31,10 +31,19 @@ uniqueHostname="${GITHUB_JOB:-local}-${imageVariant}-${GITHUB_RUN_ID:-0}" # Sanitize hostname: replace underscores and spaces with hyphens, ensure lowercase uniqueHostname=$(echo "$uniqueHostname" | tr '[:upper:]_ ' '[:lower:]--' | tr -s '-') +# Capture mode (see docs/envelope-capture.md): sentry-cli runs inside this container but the capture +# server runs on the host, so the container shares the host network to reach it. `--hostname` and +# `--network host` are mutually exclusive, hence the either/or. Port matches capture-corpus.ps1. +if [ -n "${SENTRY_CAPTURE_PATH:-}" ]; then + networkArgs=(--network host -e SENTRY_URL="http://127.0.0.1:8787" -e SENTRY_CAPTURE_PATH="${SENTRY_CAPTURE_PATH}") +else + networkArgs=(--hostname "$uniqueHostname") +fi + # We use the host dotnet installation - it's much faster than installing inside the docker container. set -x docker run -td --name $container \ - --network host \ + "${networkArgs[@]}" \ --user $uid:$gid \ -v "$cwd":/sentry-unity \ -v $ANDROID_HOME:$ANDROID_HOME \ @@ -44,7 +53,6 @@ docker run -td --name $container \ -e UNITY_VERSION=$unityVersion \ -e GITHUB_ACTIONS="${GITHUB_ACTIONS}" \ -e SENTRY_AUTH_TOKEN="${SENTRY_AUTH_TOKEN:-}" \ - -e SENTRY_URL="${SENTRY_URL:-}" \ --workdir /sentry-unity $image # Generate unique machine-id to avoid any hardcoded values and license-fetch congestion diff --git a/scripts/compile-xcode-project.ps1 b/scripts/compile-xcode-project.ps1 index 891117b0b..5fa37e564 100644 --- a/scripts/compile-xcode-project.ps1 +++ b/scripts/compile-xcode-project.ps1 @@ -3,6 +3,16 @@ param ( ) . $PSScriptRoot/../test/Scripts.Integration.Test/common.ps1 +. $PSScriptRoot/../test/Scripts.Integration.Test/capture-corpus.ps1 + +# Capture mode: sentry-cli refuses to combine a URL from the environment with the auth token +# baked into sentry.properties, so both come from here. The capture server ignores the token. +if (Test-CaptureEnabled) +{ + Start-CaptureServer + $env:SENTRY_URL = $Global:CaptureUrl + $env:SENTRY_AUTH_TOKEN = "capture-mode" +} $ProjectName = "Unity-iPhone" $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index 7d0d68e08..bc3255889 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -23,6 +23,9 @@ $ErrorActionPreference = "Stop" # Import shared test cases and utility functions . $PSScriptRoot/CommonTestCases.ps1 +# Opt-in envelope capture (no-op unless SENTRY_CAPTURE_PATH is set) +. $PSScriptRoot/../Scripts.Integration.Test/capture-corpus.ps1 + BeforeAll { # Build app arguments for a given test action function Get-AppArguments { @@ -132,26 +135,6 @@ BeforeAll { return $runResult } - # Tags the envelopes that envelope-capture-server.py records next with the test action - # they belong to. No-op unless the DSN points at the local capture server. - function Set-CaptureLabel { - param ( - [Parameter(Mandatory=$true)] - [string]$Label - ) - - if (-not $script:CaptureMode) { - return - } - - try { - Invoke-WebRequest -Uri "http://127.0.0.1:8787/MARK?label=$Label" -TimeoutSec 5 -UseBasicParsing | Out-Null - } - catch { - Write-Host "Failed to mark capture label '$Label': $_" - } - } - # Run integration test action function Invoke-TestAction { param ( @@ -220,15 +203,17 @@ BeforeAll { throw "SENTRY_DSN environment variable is not set." } - # Envelope capture mode: the DSN points at envelope-capture-server.py instead of Sentry, so the - # test actions still run (and their raw envelopes get recorded) but there is no backend to verify - # against. The event assertions below fail by design in this mode - the artifact is the corpus. - $script:CaptureMode = $env:SENTRY_DSN -match '://[^@]*@(127\.0\.0\.1|localhost|10\.0\.2\.2)' - if ($script:CaptureMode) { - Write-Host "Envelope capture mode: DSN points at the local capture server, skipping Sentry API verification." -ForegroundColor Yellow + # Capture mode: the app sends its envelopes to the local capture server instead of Sentry, so the + # test actions still run (and their payloads get recorded) but there is no backend to verify + # against. The event assertions below fail by design in this mode - the corpus is the artifact. + # The server is started here rather than in a workflow step: one started earlier does not + # reliably survive the gap between steps. + if (Test-CaptureEnabled) { + Write-Host "Capture mode: recording the corpus, skipping Sentry API verification." -ForegroundColor Yellow + Start-CaptureServer } - if (-not $script:CaptureMode -and [string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { + if (-not (Test-CaptureEnabled) -and [string]::IsNullOrEmpty($env:SENTRY_AUTH_TOKEN)) { throw "SENTRY_AUTH_TOKEN environment variable is not set." } if ([string]::IsNullOrEmpty($env:SENTRY_TEST_APP)) { @@ -308,7 +293,7 @@ BeforeAll { AuthToken = $env:SENTRY_AUTH_TOKEN } - if (-not $script:CaptureMode) { + if (-not (Test-CaptureEnabled)) { Connect-SentryApi ` -ApiToken $script:TestSetup.AuthToken ` -DSN $script:TestSetup.Dsn @@ -317,7 +302,9 @@ BeforeAll { AfterAll { - if (-not $script:CaptureMode) { + Stop-CaptureServer + + if (-not (Test-CaptureEnabled)) { Disconnect-SentryApi } if ($script:Platform -ne "WebGL") { @@ -334,7 +321,7 @@ Describe "Unity $($env:SENTRY_TEST_PLATFORM) Integration Tests" { $script:runResult = Invoke-TestAction -Action "message-capture" $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId -and -not $script:CaptureMode) { + if ($eventId -and -not (Test-CaptureEnabled)) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -EventId "$eventId" Write-Host "::endgroup::" @@ -360,7 +347,7 @@ Describe "Unity $($env:SENTRY_TEST_PLATFORM) Integration Tests" { $script:runResult = Invoke-TestAction -Action "exception-capture" $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId -and -not $script:CaptureMode) { + if ($eventId -and -not (Test-CaptureEnabled)) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -EventId "$eventId" Write-Host "::endgroup::" @@ -422,7 +409,7 @@ if ($env:SENTRY_TEST_PLATFORM -ne "WebGL") { } $eventId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($eventId -and -not $script:CaptureMode) { + if ($eventId -and -not (Test-CaptureEnabled)) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -TagName "test.crash_id" -TagValue "$eventId" -TimeoutSeconds 300 Write-Host "::endgroup::" @@ -478,7 +465,7 @@ if ($env:SENTRY_TEST_PLATFORM -in "Desktop", "Android" -and -not $isCocoaBackend # The native app-hang event is captured in-proc (same run, no relaunch). Its event ID # is generated natively, so look it up by the unique scope tag the app sets instead. $hangId = Get-EventIds -AppOutput $script:runResult.Output -ExpectedCount 1 - if ($hangId -and -not $script:CaptureMode) { + if ($hangId -and -not (Test-CaptureEnabled)) { Write-Host "::group::Getting event content" $script:runEvent = Get-SentryTestEvent -TagName "test.app_hang_id" -TagValue "$hangId" -TimeoutSeconds 300 Write-Host "::endgroup::" diff --git a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs index 6c7c46d6f..89b72cd1f 100644 --- a/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs +++ b/test/Scripts.Integration.Test/Scripts/CliConfiguration.cs @@ -16,9 +16,6 @@ public override void Configure(SentryCliOptions cliOptions) cliOptions.Organization = "sentry-sdks"; cliOptions.Project = "sentry-unity-integration-tests"; - // No UrlOverride on purpose: sentry-cli derives its upload URL from the DSN, so during - // envelope capture the debug files go to the capture server and end up in the artifact. - Debug.Log("Sentry: CliConfiguration::Configure() finished"); } } diff --git a/test/Scripts.Integration.Test/build-project.ps1 b/test/Scripts.Integration.Test/build-project.ps1 index 35a287637..3ded48a89 100644 --- a/test/Scripts.Integration.Test/build-project.ps1 +++ b/test/Scripts.Integration.Test/build-project.ps1 @@ -11,6 +11,16 @@ if (-not $Global:NewProjectPathCache) } . $PSScriptRoot/common.ps1 +. $PSScriptRoot/capture-corpus.ps1 + +# Capture mode: sentry-cli uploads the debug files during the build, so the server has to be up +# for its duration. SENTRY_URL is what redirects it - sentry-cli 3.x ignores the `defaults.url` +# the SDK writes into sentry.properties. +if (Test-CaptureEnabled) +{ + Start-CaptureServer + $env:SENTRY_URL = $Global:CaptureUrl +} $unityPath = FormatUnityPath $UnityPath $buildMethod = BuildMethodFor $Platform diff --git a/test/Scripts.Integration.Test/capture-corpus.ps1 b/test/Scripts.Integration.Test/capture-corpus.ps1 new file mode 100644 index 000000000..0c62a5047 --- /dev/null +++ b/test/Scripts.Integration.Test/capture-corpus.ps1 @@ -0,0 +1,133 @@ +#!/usr/bin/env pwsh +# +# Opt-in capture of the raw envelopes and debug files the integration tests produce, so they can be +# replayed against a local Sentry. See docs/envelope-capture.md. +# +# Everything keys off one environment variable: when SENTRY_CAPTURE_PATH points at a directory, the +# integration scripts route the SDK (envelopes, at run time) and sentry-cli (debug files, at build +# time) to a local capture server writing into it. Unset, every function here is a no-op and the +# tests behave exactly as they always have. +# +# Dot-source this file to use it: +# . $PSScriptRoot/capture-corpus.ps1 + +$Global:CapturePort = 8787 +$Global:CaptureUrl = "http://127.0.0.1:$Global:CapturePort" +# The key is irrelevant - the capture server accepts anything - but the DSN has to parse. +$Global:CaptureDsn = "http://capture@127.0.0.1:$Global:CapturePort/1" + +function Test-CaptureEnabled +{ + return -not [string]::IsNullOrEmpty($env:SENTRY_CAPTURE_PATH) +} + +# Starts the capture server unless one is already serving. Call this from whatever step actually +# needs it: a server started in an earlier CI step does not reliably survive the gap - the runner +# leaves it suspended, holding the port without answering, which surfaces as an empty reply. +function Start-CaptureServer +{ + if (-not (Test-CaptureEnabled)) + { + return + } + + if (Test-CaptureServerHealthy) + { + Write-Host "Capture server already running on port $Global:CapturePort" + return + } + + Clear-CapturePort + + $python = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } + $server = Join-Path $PSScriptRoot "envelope-capture-server.py" + $output = $env:SENTRY_CAPTURE_PATH + + New-Item -ItemType Directory -Force -Path $output | Out-Null + Start-Process -FilePath $python ` + -ArgumentList @($server, "--output", $output, "--port", $Global:CapturePort, + "--platform", (Split-Path $output -Leaf)) ` + -RedirectStandardError (Join-Path $output "capture-server.log") -NoNewWindow + + for ($i = 1; $i -le 30; $i++) + { + if (Test-CaptureServerHealthy) + { + Write-Host "Capture server is up on port $Global:CapturePort (writing to $output)" + return + } + Start-Sleep -Seconds 1 + } + + Get-Content (Join-Path $output "capture-server.log") -ErrorAction SilentlyContinue | Write-Host + throw "Capture server did not come up on port $Global:CapturePort" +} + +# Tags the files captured next with the test action they belong to, so the corpus is browsable. +function Set-CaptureLabel +{ + param([Parameter(Mandatory = $true)][string] $Label) + + if (-not (Test-CaptureEnabled)) + { + return + } + + try + { + Invoke-WebRequest -Uri "$Global:CaptureUrl/MARK?label=$Label" -TimeoutSec 5 -UseBasicParsing | Out-Null + } + catch + { + Write-Host "Failed to mark capture label '$Label': $_" + } +} + +function Stop-CaptureServer +{ + if (-not (Test-CaptureEnabled)) + { + return + } + + try + { + Invoke-WebRequest -Uri "$Global:CaptureUrl/STOP" -TimeoutSec 5 -UseBasicParsing | Out-Null + } + catch + { + Write-Host "Capture server already gone" + } +} + +function Test-CaptureServerHealthy +{ + try + { + Invoke-WebRequest -Uri "$Global:CaptureUrl/HEALTH" -TimeoutSec 2 -UseBasicParsing | Out-Null + return $true + } + catch + { + return $false + } +} + +# A suspended server from an earlier step keeps the port bound, which would make the new one fail +# with "Address already in use". +function Clear-CapturePort +{ + if ($IsWindows) + { + Get-NetTCPConnection -LocalPort $Global:CapturePort -State Listen -ErrorAction SilentlyContinue | + ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } + } + else + { + foreach ($processId in (& lsof -ti "tcp:$Global:CapturePort" 2>$null)) + { + Write-Host "Killing stale listener on port $Global:CapturePort (pid $processId)" + & kill -9 $processId 2>$null + } + } +} diff --git a/test/Scripts.Integration.Test/configure-sentry.ps1 b/test/Scripts.Integration.Test/configure-sentry.ps1 index 21c04aa26..b0c6e746f 100644 --- a/test/Scripts.Integration.Test/configure-sentry.ps1 +++ b/test/Scripts.Integration.Test/configure-sentry.ps1 @@ -9,6 +9,13 @@ if (-not $Global:NewProjectPathCache) } . $PSScriptRoot/common.ps1 +. $PSScriptRoot/capture-corpus.ps1 + +# Capture mode: the app sends its envelopes to the local capture server instead of Sentry. +if (Test-CaptureEnabled) +{ + $env:SENTRY_DSN = $Global:CaptureDsn +} $UnityPath = FormatUnityPath $UnityPath diff --git a/test/Scripts.Integration.Test/start-capture-server.ps1 b/test/Scripts.Integration.Test/start-capture-server.ps1 deleted file mode 100644 index ae0ac95ba..000000000 --- a/test/Scripts.Integration.Test/start-capture-server.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env pwsh -# -# Starts envelope-capture-server.py in the background and waits until it serves. -# Used by the integration test workflows when SENTRY_DSN points at the capture host. - -param( - [string] $Platform = "unknown", - [int] $Port = 8787, - [string] $Output = "" -) - -$ErrorActionPreference = "Stop" - -# One directory per platform so the per-job artifacts can be merged into a single corpus -# without index.jsonl and capture-server.log colliding. -if ([string]::IsNullOrEmpty($Output)) { - $Output = "test/IntegrationTest/envelopes/$Platform" -} - -# Build jobs call this from every build step, because a detached server does not reliably survive -# the gap between steps. Reuse the running one instead of fighting over the port and the log file. -try { - Invoke-WebRequest -Uri "http://127.0.0.1:$Port/HEALTH" -TimeoutSec 2 -UseBasicParsing | Out-Null - Write-Host "Envelope capture server already running on port $Port" - exit 0 -} -catch { - # Nothing answered. A server from a previous step may still be holding the port without - # serving (the runner suspends leftovers between steps), which would make the new one fail - # with "Address already in use" - so clear the port before starting. - if ($IsWindows) { - Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | - ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue } - } - else { - $stale = & lsof -ti "tcp:$Port" 2>$null - foreach ($processId in $stale) { - Write-Host "Killing stale listener on port $Port (pid $processId)" - & kill -9 $processId 2>$null - } - } -} - -$python = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } -$server = Join-Path $PSScriptRoot "envelope-capture-server.py" - -New-Item -ItemType Directory -Force -Path $Output | Out-Null -$logPath = Join-Path $Output "capture-server.log" - -Start-Process -FilePath $python ` - -ArgumentList @($server, "--output", $Output, "--port", $Port, "--platform", $Platform) ` - -RedirectStandardError $logPath -NoNewWindow - -for ($i = 1; $i -le 30; $i++) { - try { - Invoke-WebRequest -Uri "http://127.0.0.1:$Port/HEALTH" -TimeoutSec 2 -UseBasicParsing | Out-Null - Write-Host "Envelope capture server is up on port $Port (writing to $Output)" - exit 0 - } - catch { - Start-Sleep -Seconds 1 - } -} - -Get-Content $logPath -ErrorAction SilentlyContinue | Write-Host -throw "Envelope capture server did not come up on port $Port" From 2f6508abd4099d435e7cad95214eeee952ef2f68 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 14:00:51 +0200 Subject: [PATCH 13/18] TEMPORARY: force capture on for validation --- .github/workflows/ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc2392bd7..969135b05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: env: UNITY_PATH: docker exec unity unity-editor # Empty unless a capture run was dispatched; see docs/envelope-capture.md. - SENTRY_CAPTURE_PATH: ${{ inputs.capture-corpus && format('test/IntegrationTest/capture/webgl-{0}', matrix.unity-version) || '' }} + SENTRY_CAPTURE_PATH: ${{ format('test/IntegrationTest/capture/webgl-{0}', matrix.unity-version) }} # TEMPORARY steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -308,7 +308,7 @@ jobs: uses: ./.github/workflows/test-build-android.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-run-android: name: Run Android ${{ matrix.unity-version }} Integration Test @@ -322,7 +322,7 @@ jobs: unity-version: ${{ matrix.unity-version }} api-level: ${{ matrix.api-level }} init-type: ${{ matrix.init-type }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR strategy: fail-fast: false matrix: @@ -345,7 +345,7 @@ jobs: uses: ./.github/workflows/test-build-ios.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-compile-ios: name: Compile iOS ${{ matrix.unity-version }} Test @@ -360,7 +360,7 @@ jobs: with: unity-version: ${{ matrix.unity-version }} init-type: ${{ matrix.init-type }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-run-ios: name: Run iOS ${{ matrix.unity-version }} Integration Test @@ -371,7 +371,7 @@ jobs: unity-version: ${{ matrix.unity-version }} ios-version: ${{ matrix.ios-version }} init-type: ${{ matrix.init-type }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} @@ -406,7 +406,7 @@ jobs: uses: ./.github/workflows/test-run-webgl.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-build-linux: name: Build Linux ${{ matrix.unity-version }} Integration Test @@ -423,7 +423,7 @@ jobs: uses: ./.github/workflows/test-build-linux.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-build-windows: name: Build Windows ${{ matrix.unity-version }} Integration Test @@ -440,7 +440,7 @@ jobs: uses: ./.github/workflows/test-build-windows.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-build-macos: name: Build macOS ${{ matrix.unity-version }} Integration Test @@ -457,7 +457,7 @@ jobs: uses: ./.github/workflows/test-build-macos.yml with: unity-version: ${{ matrix.unity-version }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-run-linux: name: Run Linux ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -476,7 +476,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: linux backend: ${{ matrix.backend }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-run-windows: name: Run Windows ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -495,7 +495,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: windows backend: ${{ matrix.backend }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR test-run-macos: name: Run macOS ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -514,7 +514,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: macos backend: ${{ matrix.backend }} - capture: ${{ inputs.capture-corpus || false }} + capture: true # TEMPORARY: validating the capture wiring on this PR build-size-summary: name: Build Size From 8c913b76af02c6192c294e59bdd9d535e68a472b Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 15:34:44 +0200 Subject: [PATCH 14/18] keep lsof exit code from failing the build step --- test/Scripts.Integration.Test/capture-corpus.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/Scripts.Integration.Test/capture-corpus.ps1 b/test/Scripts.Integration.Test/capture-corpus.ps1 index 0c62a5047..f683f0f70 100644 --- a/test/Scripts.Integration.Test/capture-corpus.ps1 +++ b/test/Scripts.Integration.Test/capture-corpus.ps1 @@ -129,5 +129,9 @@ function Clear-CapturePort Write-Host "Killing stale listener on port $Global:CapturePort (pid $processId)" & kill -9 $processId 2>$null } + + # `lsof` exits 1 when nothing matches, which is the normal case here. Left alone that + # becomes the exit code of the whole calling step, failing a build that actually succeeded. + $global:LASTEXITCODE = 0 } } From b996a28a0267339438df477432ebe0ac1a1c2478 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Tue, 18 Aug 2026 16:52:33 +0200 Subject: [PATCH 15/18] source capture module inside pester blocks --- test/IntegrationTest/Integration.Tests.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index bc3255889..c0afd8c42 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -23,10 +23,12 @@ $ErrorActionPreference = "Stop" # Import shared test cases and utility functions . $PSScriptRoot/CommonTestCases.ps1 -# Opt-in envelope capture (no-op unless SENTRY_CAPTURE_PATH is set) -. $PSScriptRoot/../Scripts.Integration.Test/capture-corpus.ps1 - BeforeAll { + # Opt-in capture (no-op unless SENTRY_CAPTURE_PATH is set). Dot-sourced here rather than at + # script level because Pester runs this block in a scope that does not see script-level + # functions. + . $PSScriptRoot/../Scripts.Integration.Test/capture-corpus.ps1 + # Build app arguments for a given test action function Get-AppArguments { param([string]$Action) @@ -302,6 +304,7 @@ BeforeAll { AfterAll { + . $PSScriptRoot/../Scripts.Integration.Test/capture-corpus.ps1 Stop-CaptureServer if (-not (Test-CaptureEnabled)) { From 8c6e216cbded30ecac63657f8e4ad7801fbedd4d Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Wed, 19 Aug 2026 10:16:48 +0200 Subject: [PATCH 16/18] tunnel the capture port to android devices --- test/IntegrationTest/Integration.Tests.ps1 | 1 + test/Scripts.Integration.Test/capture-corpus.ps1 | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/test/IntegrationTest/Integration.Tests.ps1 b/test/IntegrationTest/Integration.Tests.ps1 index c0afd8c42..602579cd7 100644 --- a/test/IntegrationTest/Integration.Tests.ps1 +++ b/test/IntegrationTest/Integration.Tests.ps1 @@ -232,6 +232,7 @@ BeforeAll { Connect-Device -Platform "Adb" Install-DeviceApp -Path $env:SENTRY_TEST_APP + Connect-CaptureToDevice # Detect the launcher activity from the installed package $dumpOutput = & adb shell dumpsys package $script:PackageName 2>&1 | Out-String diff --git a/test/Scripts.Integration.Test/capture-corpus.ps1 b/test/Scripts.Integration.Test/capture-corpus.ps1 index f683f0f70..cd1b89372 100644 --- a/test/Scripts.Integration.Test/capture-corpus.ps1 +++ b/test/Scripts.Integration.Test/capture-corpus.ps1 @@ -63,6 +63,20 @@ function Start-CaptureServer throw "Capture server did not come up on port $Global:CapturePort" } +# Android runs the app on a device or emulator, where 127.0.0.1 is the device itself. Tunnel the +# capture port back to this host so the SDK's envelopes reach the server. Safe to call repeatedly. +function Connect-CaptureToDevice +{ + if (-not (Test-CaptureEnabled)) + { + return + } + + & adb reverse "tcp:$Global:CapturePort" "tcp:$Global:CapturePort" 2>&1 | Write-Host + # `adb` is a native command; don't let its exit code become the calling step's. + $global:LASTEXITCODE = 0 +} + # Tags the files captured next with the test action they belong to, so the corpus is browsable. function Set-CaptureLabel { From 50be50ce4ea7186608f7d9adb4e1208ced135ad7 Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Thu, 20 Aug 2026 11:03:38 +0200 Subject: [PATCH 17/18] Revert "TEMPORARY: force capture on for validation" This reverts commit 2f6508abd4099d435e7cad95214eeee952ef2f68. --- .github/workflows/ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969135b05..bc2392bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: env: UNITY_PATH: docker exec unity unity-editor # Empty unless a capture run was dispatched; see docs/envelope-capture.md. - SENTRY_CAPTURE_PATH: ${{ format('test/IntegrationTest/capture/webgl-{0}', matrix.unity-version) }} # TEMPORARY + SENTRY_CAPTURE_PATH: ${{ inputs.capture-corpus && format('test/IntegrationTest/capture/webgl-{0}', matrix.unity-version) || '' }} steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -308,7 +308,7 @@ jobs: uses: ./.github/workflows/test-build-android.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-run-android: name: Run Android ${{ matrix.unity-version }} Integration Test @@ -322,7 +322,7 @@ jobs: unity-version: ${{ matrix.unity-version }} api-level: ${{ matrix.api-level }} init-type: ${{ matrix.init-type }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} strategy: fail-fast: false matrix: @@ -345,7 +345,7 @@ jobs: uses: ./.github/workflows/test-build-ios.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-compile-ios: name: Compile iOS ${{ matrix.unity-version }} Test @@ -360,7 +360,7 @@ jobs: with: unity-version: ${{ matrix.unity-version }} init-type: ${{ matrix.init-type }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-run-ios: name: Run iOS ${{ matrix.unity-version }} Integration Test @@ -371,7 +371,7 @@ jobs: unity-version: ${{ matrix.unity-version }} ios-version: ${{ matrix.ios-version }} init-type: ${{ matrix.init-type }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} secrets: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_TEST_DSN: ${{ secrets.SENTRY_TEST_DSN }} @@ -406,7 +406,7 @@ jobs: uses: ./.github/workflows/test-run-webgl.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-build-linux: name: Build Linux ${{ matrix.unity-version }} Integration Test @@ -423,7 +423,7 @@ jobs: uses: ./.github/workflows/test-build-linux.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-build-windows: name: Build Windows ${{ matrix.unity-version }} Integration Test @@ -440,7 +440,7 @@ jobs: uses: ./.github/workflows/test-build-windows.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-build-macos: name: Build macOS ${{ matrix.unity-version }} Integration Test @@ -457,7 +457,7 @@ jobs: uses: ./.github/workflows/test-build-macos.yml with: unity-version: ${{ matrix.unity-version }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-run-linux: name: Run Linux ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -476,7 +476,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: linux backend: ${{ matrix.backend }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-run-windows: name: Run Windows ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -495,7 +495,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: windows backend: ${{ matrix.backend }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} test-run-macos: name: Run macOS ${{ matrix.backend }} ${{ matrix.unity-version }} Integration Test @@ -514,7 +514,7 @@ jobs: unity-version: ${{ matrix.unity-version }} platform: macos backend: ${{ matrix.backend }} - capture: true # TEMPORARY: validating the capture wiring on this PR + capture: ${{ inputs.capture-corpus || false }} build-size-summary: name: Build Size From a75171fd5b4f1a19fa0fac359082bdb4fef0f94e Mon Sep 17 00:00:00 2001 From: bitsandfoxes Date: Fri, 21 Aug 2026 13:08:49 +0200 Subject: [PATCH 18/18] il2cpp line mappings & proguard --- docs/envelope-capture.md | 49 +++++++++++++++-- .../envelope-capture-server.py | 52 ++++++++++++++----- 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/docs/envelope-capture.md b/docs/envelope-capture.md index a742f4e6c..af290a556 100644 --- a/docs/envelope-capture.md +++ b/docs/envelope-capture.md @@ -2,8 +2,9 @@ The integration tests exercise every managed error, native crash and app hang path the SDK has, on every platform we ship. Capture mode records what those runs actually put on the wire - raw -envelopes, minidump uploads, and the debug files sentry-cli uploads - so the whole lot can be -replayed against a local Sentry to work on event processing or symbolication. +envelopes, minidump uploads, and the debug files, source bundles and IL2CPP line mappings +sentry-cli uploads - so the whole lot can be replayed against a local Sentry to work on event +processing or symbolication. It is **off by default** and changes nothing about a normal CI run. @@ -19,12 +20,25 @@ gh run download -p 'corpus-*' -D ./corpus | Artifact | Contains | |---|---| -| `corpus--` | debug files and source bundles sentry-cli uploaded for that build | +| `corpus--` | debug files, source bundles and IL2CPP line mappings sentry-cli uploaded for that build | | `corpus-run---` | the envelopes and minidump uploads that run produced | Debug files are large (IL2CPP `GameAssembly.pdb` and friends run to hundreds of MB per platform), so they stay per-job rather than being merged into one download. +Everything sentry-cli uploaded lands in `debug-files/`, named +`--` and suffixed by kind: + +| Suffix | Kind | | +|---|---|---| +| *(none)* | `debug-file` | the dSYM / PDB / ELF itself | +| `.src` | `source-bundle` | the sources, from `--include-sources` | +| `.il2cpp.json` | `il2cpp-line-mapping` | C++ → C# line mapping, from `--il2cpp-mapping` | +| `.proguard` | `proguard-mapping` | Android `mapping.txt`, from `upload-proguard` | + +`debug-files/index.jsonl` records the `kind` alongside the assemble request, and the server prints +a per-kind tally when it shuts down. + **The event assertions fail by design in a capture run.** There is no backend to verify against, so `Integration.Tests.ps1` skips the Sentry API lookups and every event assertion fails. The artifacts are the deliverable; a red run is expected. @@ -54,6 +68,12 @@ Pass `--keep-ids` / `--keep-timestamps` to replay the bytes as they were capture Minidump uploads are replayed verbatim to `/api//minidump/` with only the ingest key swapped - the event ids inside the multipart body are left alone. +`debug-files upload` re-uploads the difs and source bundles, but **not** the `.il2cpp.json` +mappings: sentry-cli only picks up files it recognises as difs, and it recomputes mappings from the +generated C++ next to the object rather than from a mapping file. The C++ is not in the corpus, so +the captured `.il2cpp.json` is the only copy - read it directly, or POST it to the chunk-upload and +`files/difs/assemble/` endpoints the way sentry-cli does. + ## How it works Everything keys off one environment variable, **`SENTRY_CAPTURE_PATH`**. The workflows set it from @@ -82,6 +102,29 @@ Details worth knowing if any of this regresses: currently upload their symbols to sentry.io; worth fixing upstream. - The iOS Xcode phase additionally needs `SENTRY_AUTH_TOKEN` from the environment, because sentry-cli refuses to combine a URL from the environment with a token from `sentry.properties`. +- IL2CPP line mappings need no wiring of their own: `--il2cpp-mapping` is part of the same + `debug-files upload` the difs go through ([`BuildPostProcess`](../src/Sentry.Unity.Editor/Native/BuildPostProcess.cs), + [`DebugSymbolUpload`](../src/Sentry.Unity.Editor/Android/DebugSymbolUpload.cs), + [`SentryXcodeProject`](../src/Sentry.Unity.Editor.iOS/SentryXcodeProject.cs)), and they are chunked + and assembled like everything else. They are told apart **by content**: assemble carries only a + name, a debug id and the chunks, and a mapping inherits name and debug id from the object it was + computed from, so only the payload distinguishes them (`SYSB` magic, or a leading `{` for the + mapping JSON). +- Android proguard mappings ride the same path despite being a separate `upload-proguard` + invocation: sentry-cli chunk-uploads them and assembles them through `files/difs/assemble/` like + everything else. They are the one kind identifiable by metadata - sentry-cli names them + `/proguard/.txt` and sends no debug id, hence the `unknown-` prefix in the corpus. +- A proguard mapping only exists when minification is on: `sentryUploadProguardMapping` is + registered from [`AndroidUtils.ShouldUploadMapping`](../src/Sentry.Unity.Editor/Android/AndroidUtils.cs), + which reads `PlayerSettings.Android.minifyRelease` (release, because the test builds set + `EditorUserBuildSettings.development = false`). The integration test turns both minify flags on + in [`Builder.cs`](../test/Scripts.Integration.Test/Editor/Builder.cs), so an Android capture run + is expected to show a `proguard-mapping` in the tally. If it does not, check that flag first. +- A capture run where the tally shows difs but no `il2cpp-line-mapping` means IL2CPP line numbers + regressed upstream of the upload - either `--emit-source-mapping` never reached il2cpp + ([`Il2CppBuildPreProcess`](../src/Sentry.Unity.Editor/Il2CppBuildPreProcess.cs), gated on + `Il2CppLineNumberSupportEnabled`), or the generated C++ was gone by the time sentry-cli ran, since + it reads the `source_info` comments back out of those files. - Capture listens on **8787**; `webgl-server.py` already serves the WebGL build on 8000. - macOS ATS blocks plain HTTP to an IP literal, so the test app's `Info.plist` gets `NSAllowsArbitraryLoads` ([`AllowInsecureHttp.cs`](../test/Scripts.Integration.Test/Editor/AllowInsecureHttp.cs)). diff --git a/test/Scripts.Integration.Test/envelope-capture-server.py b/test/Scripts.Integration.Test/envelope-capture-server.py index 5463e506d..1485bd101 100644 --- a/test/Scripts.Integration.Test/envelope-capture-server.py +++ b/test/Scripts.Integration.Test/envelope-capture-server.py @@ -40,6 +40,7 @@ symbol_dir = Path(".") platform_name = "unknown" assembled = set() +kinds = {} def parse_envelope(data): @@ -105,6 +106,26 @@ def safe(value): return re.sub(r"[^A-Za-z0-9_.-]", "_", value)[:60] or "unknown" +def classify(name, magic): + """Names the kind of file being assembled, and the suffix that marks it in the corpus. + + Assemble tells us only name, debug id and chunks. `upload-proguard` announces itself in the + name, but a source bundle and an IL2CPP line mapping inherit both name and debug id from the + object they were computed from, so their kind has to come from the content. + """ + # sentry-cli assembles proguard mappings as `/proguard/.txt`, with no debug id. + if name.startswith("/proguard/"): + return "proguard-mapping", ".proguard" + if magic.startswith(b"SYSB"): + return "source-bundle", ".src" + # `--il2cpp-mapping` uploads the line mapping as a plain JSON object: + # {"": {"": {"": }}, "__debug-id__": {...}}. + # No debug file format starts with a brace, so that alone tells them apart. + if magic.startswith(b"{"): + return "il2cpp-line-mapping", ".il2cpp.json" + return "debug-file", "" + + class Handler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -207,7 +228,8 @@ def handle_assemble(self, body): response = {} for checksum, entry in request.items(): - name = Path(entry.get("name") or checksum).name + requested_name = entry.get("name") or checksum + name = Path(requested_name).name # sentry-cli polls assemble until every file reports `ok`. Once assembled we drop the # chunks, so answer from this set rather than re-checking them - otherwise the next @@ -222,32 +244,32 @@ def handle_assemble(self, body): response[checksum] = {"state": "not_found", "missingChunks": missing, "detail": None} continue - # A dif and its source bundle share both debug id and name, so the checksum keeps - # them from overwriting each other. + # A dif, its source bundle and its IL2CPP line mapping all share debug id and name, + # so the checksum keeps them from overwriting each other. target = symbol_dir / f"{entry.get('debug_id', 'unknown')}-{checksum[:8]}-{safe(name)}" with target.open("wb") as out: for chunk in entry["chunks"]: out.write((chunk_dir / chunk).read_bytes()) - # Source bundles carry Sentry's "SYSB" magic; mark them so the corpus is self-describing. # The handle has to be closed before renaming - Windows refuses to rename an open file. with target.open("rb") as probe: - is_source_bundle = probe.read(4) == b"SYSB" - if is_source_bundle: - # replace(), not rename(): a second build re-uploads the same bundles and Windows + kind, suffix = classify(requested_name, probe.read(4)) + if suffix: + # replace(), not rename(): a second build re-uploads the same files and Windows # refuses to rename onto an existing file. - target = target.replace(target.with_name(target.name + ".src")) + target = target.replace(target.with_name(target.name + suffix)) # Chunks deliberately stay until shutdown: they are deduplicated by hash, so deleting # them here breaks any other file that shares one and makes sentry-cli fail the upload # with "Some uploaded files are now missing on the server". - print(f"assembled {target.name} ({target.stat().st_size} bytes)", file=sys.stderr) + print(f"assembled {kind} {target.name} ({target.stat().st_size} bytes)", file=sys.stderr) with state_lock: assembled.add(checksum) + kinds[kind] = kinds.get(kind, 0) + 1 with (symbol_dir / "index.jsonl").open("a") as index: - index.write(json.dumps({"file": target.name, "platform": platform_name, - "checksum": checksum, "size": target.stat().st_size, - "request": entry}) + "\n") + index.write(json.dumps({"file": target.name, "kind": kind, + "platform": platform_name, "checksum": checksum, + "size": target.stat().st_size, "request": entry}) + "\n") response[checksum] = {"state": "ok", "missingChunks": [], "detail": None} @@ -359,6 +381,12 @@ def main(): server.serve_forever() shutil.rmtree(chunk_dir, ignore_errors=True) print(f"envelope capture stopped after {sequence} requests", file=sys.stderr) + for kind, count in sorted(kinds.items()): + print(f" {kind}: {count}", file=sys.stderr) + # A build that uploaded difs but no mapping means the IL2CPP line numbers regressed: either + # `--emit-source-mapping` never reached il2cpp, or the generated C++ was gone by upload time. + if kinds and "il2cpp-line-mapping" not in kinds: + print(" WARNING: no IL2CPP line mappings were uploaded", file=sys.stderr) if __name__ == "__main__":