Core/Dash CI/CD Performance-Checks

Stoppe eine schwere Seite, bevor sie live geht. Lass dir dann von echten Nutzerdaten zeigen, was jedes Release bewirkt hat.

Kostenlos testen

Trusted by market leaders · Client results

adevintaworkivasaturncomparevpnnina careharvardmonarchfotocasaaleteiaebaykpnsnvloopearplugsmy work featured on web.devmarktplaatsnestlewhowhatwearperiondpg mediaerasmusmchappyhorizon

Welches Deploy hat es langsamer gemacht?

Dein LCP lag im Juni bei 2,1s. Jetzt liegt er bei 2,9s. In diesen acht Wochen hast du vierzigmal ausgeliefert. Nichts in deinem Monitoring sagt dir, welches dieser vierzig Deploys der Auslöser war.

Das ist kein Datenproblem. CoreDash misst bereits jeden Seitenaufruf jedes echten Besuchers. Was es nicht allein herausfinden kann, ist, was sich bei dir an einem Dienstag um 14:32 Uhr geändert hat. Ohne das bleibt eine Regression „etwas wurde letzten Monat langsamer“ statt „v2.4.1 hat das verursacht“. Also suchst du aus dem Gedächtnis, fragst das Team, was ausgeliefert wurde, gibst meistens auf und optimierst etwas Unbeteiligtes.

Die CI/CD-Integration schließt diese Lücke aus zwei Richtungen. Vor einem Deploy scannt Core/Dash deinen Preview-Build und stoppt die Pipeline, wenn eine Seite das Budget überschreitet. Nach einem Deploy wird jeder Seitenaufruf mit der ausliefernden Version getaggt. So wird jedes Release gegen seinen eigenen Traffic gemessen.

Beides ist optional und unabhängig. Nutze eines, beides oder keines. Für keines musst du eine Zeile Frontend-Code ändern oder ein zweites Skript zu deinen Seiten hinzufügen.

Zwei Checks rund um ein Deploy

Die zwei Hälften fangen unterschiedliche Fehler ab. Diesen Teil solltest du verstehen, bevor du etwas anbindest.

Der Pre-Deploy-Check ist synthetisch. Er lädt dein Preview-Deployment in Lighthouse. Er prüft Dinge, die für den Build selbst gelten: wie viele Bytes die Seite ausliefert, wie viel Skript, wie groß die Bilder sind, wie viele DOM-Knoten es gibt. Das sind die Regressionen, die ein menschliches Review übersieht. Niemand bemerkt in einem Diff, dass das neue Hero-Bild ein 4 MB großes PNG ist. Oder dass ein Marketing-Tag 300 KB extra JavaScript ins Bundle gezogen hat.

Der Release-Vergleich ist field data. Echte Geräte, echte Netzwerke, echte Nutzer, gemessen nach dem Livegang. Nur dort existieren LCP, INP und CLS wirklich. Ein synthetischer Lauf auf der Infrastruktur von Google kann dir nicht sagen, dass dein indonesischer Android-Traffic 900 ms langsamer wurde. Ein Rechenzentrum in Iowa ist eben kein Android-Smartphone in einem 4G-Netz.

Du willst beides, weil keines das andere ersetzt. Synthetische Tests fangen die offensichtlichen Fehler früh und günstig ab. Field data zeigt dir, was deine Nutzer erhalten haben. Und das ist das Einzige, worauf Google das Ranking stützt.

Vor dem Deploy: der Pre-Deploy-Check

Ein POST aus deiner Pipeline gegen deine Preview-URL. Core/Dash führt Lighthouse gegen deine überwachten Seiten auf diesem Preview-Origin aus. Es bewertet sie gegen deine bestehenden Lighthouse-Budgets und antwortet mit Pass oder Breach. Das dauert etwa 30 bis 45 Sekunden.

Was geprüft wird

Core/Dash nimmt die ersten vier überwachten Lighthouse-Seiten, die eine bewertbare Budgetgrenze haben. Es behält den eigenen Pfad, den Query-String und die Geräteeinstellung jeder Seite bei. Dann tauscht es den Host gegen den von dir gesendeten Preview-Origin aus. Die geprüfte Seite ist also dieselbe Route, die dein nächtlicher Scan prüft. Nur eben auf dem Build, der noch nicht live ist.

Vier Seiten sind das Limit, und zwar ganz bewusst. Die vier Scans laufen parallel gegen die PageSpeed Insights-API. Jeder ist auf 75 Sekunden begrenzt. So kann eine einzelne langsame Seite deinen Build nicht aufhalten.

Was bewertet wird und was nicht

Nur Messwerte, die sich zwischen zwei Durchläufen desselben Builds nicht verändern:

BewertetNicht bewertet
Gesamtgewicht der SeiteTotal Blocking Time
Third-Party-GewichtBootup Time
Skript-GewichtMain Thread Work
Bild-GewichtLighthouse-Performance-Score
CSS-Gewicht
Font-Gewicht
Ungenutztes JavaScript
DOM-Größe

Ein Lighthouse-Performance-Score in der CI ist fast ein Zufallszahlengenerator. Führe dieselbe unveränderte Seite fünfmal aus. Du wirst sehen, wie er um zehn Punkte schwankt. Der Score wird von CPU-gebundenen Zeiten dominiert. Die Maschine, die das Audit ausgeführt hat, ist nie zweimal dieselbe. Blockiere deine Builds damit und du bekommst rote Pipelines, die nichts bedeuten. Das trainiert jeden darauf, den Job neu zu starten, bis er grün wird. Das ist schlimmer als gar kein Check.

Bytes tun das nicht. Wenn dein Build heute 1,4 MB Skript ausliefert und morgen 1,9 MB, ist das dein Commit. Nicht die Hardware von Google. Daher bewertet der Check diese Zeilen. Die Zeitmetriken bleiben dort, wo sie hingehören: bei den echten Nutzerdaten. Dort, wo die CPU das tatsächliche Smartphone deines Besuchers ist.

Der Filter arbeitet pro Budgetgrenze. Ein Budget, das beide Arten mischt, steuert die bewertbaren Zeilen bei. Jede Antwort listet auf, was übersprungen wurde und warum.

Ruf es aus deiner Pipeline auf

Erstelle zuerst einen Projekt-API-Key (in der App: dein Projekt, dann AI Insights, dann Connect Your AI). Speichere ihn in deinen CI-Secrets als COREDASH_API_KEY. Keys beginnen mit cdk_ und sind auf ein Projekt beschränkt. Ein Master-Agency-Key wird hier abgelehnt. Er ist an kein Projekt gebunden, dem das Release zugeordnet werden könnte.

STATUS=$(curl -sS -o gate.json -w '%{http_code}' -X POST "https://app.coredash.app/api/project/releases/check" \
  -H "Authorization: Bearer $COREDASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tag":"v1.2.3","origin":"https://preview.example.app"}')

if [ "$STATUS" != "200" ]; then
  echo "Gate call failed with HTTP $STATUS: $(cat gate.json)" >&2
  exit 1
fi

jq -e '.data.check.status != "breach"' gate.json > /dev/null

Verkürze dieses Snippet nicht, indem du curl direkt an jq weiterreichst. Ein falscher Key liefert 401. Ein falsches Tag liefert 400. Diese Antworten enthalten überhaupt kein data-Objekt. Das Lesen von .data.check.status bei fehlendem data gibt dir in jq null. Null ist nicht „breach“, und die Pipe endet mit 0. Ein abgelaufener API-Key sähe genau wie ein Pass aus. Dein Gate würde stillschweigend aufhören zu blockieren. Der HTTP-Status-Check verhindert das.

Parameter

FeldPflichtfeldBeschreibung
tagjaDein Versionsstring. Buchstaben, Ziffern, . _ / -, bis zu 64 Zeichen.
originjaDer Preview-Origin, zum Beispiel https://pr-42.example.app. Nur Schema und Host.
sha, branch, actor, repo, prNumber, runUrlneinGit-Kontext. Wird für Deeplinks in der App gespeichert. Darauf basiert keine Logik.

Der Origin muss nackt sein. Ein Pfad, ein Query-String, ein Fragment oder Basic-Auth-Zugangsdaten in der URL liefern alle 400. Das ist Absicht. Der Scanner kopiert nur das Schema und den Host in die URL jeder überwachten Seite. Eine unter /pr-42/ ausgelieferte Preview würde sonst stillschweigend unter den falschen URLs gescannt werden. Sie würde einen sauberen Pass für Seiten melden, die nicht existieren.

Was zurückkommt

Ein gültiger Request liefert immer HTTP 200. Das Urteil steht im Body unter data.check.status:

StatusBedeutungDein Build
passJede bewertete Zeile liegt im BudgetFortfahren
breachMindestens eine Zeile überschreitet das BudgetStoppen
errorKeine Seite konnte überhaupt gescannt werdenFortfahren
no-budgetsKeine überwachte Seite hat ein Budget, das der Check bewerten kannFortfahren

Ein fehlerhaftes Tag oder ein fehlerhafter Origin liefern 400. Ein falscher oder fehlender Key liefert 401. Diese Bodies enthalten nur status und message.

Der Check ist fail-open, pro Seite. Wenn PageSpeed Insights bei einer deiner vier Seiten in einen Timeout läuft, wird diese Seite als Error-Eintrag gemeldet. Die anderen drei werden weiterhin bewertet. Dein Build wird nicht blockiert, nur weil Google eine schlechte Minute hatte. Der Fehler wird gemeldet statt verschluckt. So siehst du in der Antwort, welche Seite nicht gemessen wurde.

Noch zwei Dinge, die du wissen solltest. Die Preview muss aus dem öffentlichen Internet erreichbar sein. PageSpeed Insights ruft sie von der Google-Seite aus ab. Eine passwortgeschützte oder per IP-Allowlist gesicherte Preview kann nicht gescannt werden. Es gibt keinen Workaround. Wenn deine Previews hinter einer Authentifizierung liegen, führe den Check stattdessen gegen einen öffentlichen Staging-Origin aus. Und: Der Check schreibt niemals auf dein Produktions-Budget-Board. Preview-Zahlen bleiben getrennt von den Zahlen, mit denen du deine Live-Seite bewertest.

900x500?text=Pre deploy+Lighthouse+check+card+on+release+detail

Nach dem Deploy: Erfasse das Release

Sobald das Deploy erfolgreich ist, teile Core/Dash mit, dass die Version live gegangen ist:

curl -sS -X POST "https://app.coredash.app/api/project/releases/ingest" \
  -H "Authorization: Bearer $COREDASH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tag":"v1.2.3"}'

In GitHub Actions, mit der Git-Ref als Version:

- name: Report release to CoreDash
  if: success()
  env:
    COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
  run: |
    curl -sS -f -X POST "https://app.coredash.app/api/project/releases/ingest" \
      -H "Authorization: Bearer ${COREDASH_API_KEY}" \
      -H "Content-Type: application/json" \
      -d "{\"tag\":\"${{ github.ref_name }}\"}"

Jede CI, die curl ausführen kann, funktioniert. GitLab, Jenkins, CircleCI, ein Bash-Deploy-Skript auf irgendeinem Server.

FeldPflichtfeldBeschreibung
tagjaGleiche Regeln wie beim Check. Nutze denselben String, den du an den Check gesendet hast.
deployedAtneinISO-Timestamp. Standard ist jetzt. Setze ihn in die Vergangenheit, um ein bereits erfolgtes Deploy rückzudatieren.
notesneinWas ausgeliefert wurde. Bis zu 2000 Zeichen.
sha, branch, actor, repo, prNumber, runUrlneinGit-Kontext, wie beim Check.

Ein Release ist pro Projekt und Tag eindeutig. Das erneute Posten desselben Tags aktualisiert die bestehende Zeile, statt eine zweite zu erstellen. Ein Rollback, das eine Version wiederverwendet, fließt also in das dazugehörige Release zurück.

Keine Pipeline? Die Releases-Seite hat ein Tag deployment-Formular: Version, Zeitpunkt des Livegangs, optionale Notizen. Es handelt sich um dieselbe Zeile, nur mit site statt ci als Quelle. Nutze es, um bereits ausgelieferte Deploys nachzutragen. Es ist die schwächere Option, das muss man ehrlich sagen. Sie erfasst nur die Deploys, an die sich jemand erinnert einzutippen. Und die Deploy-Zeit ist nur so genau wie das Gedächtnis dieser Person.

Wie Traffic einem Release zugeordnet wird

Das Erfassen eines Releases stempelt es auf dem Projekt als aktuelle Version ab. Der Host, der dein Tracking-Skript ausliefert, liest diesen Stempel. Er injiziert das Tag in das ausgelieferte Snippet. So trägt jeder Seitenaufruf ab diesem Punkt die Version in der rel-Dimension. Die Edge-Kopie des Skripts wird aus Cloudflare gepurgt, sobald sich der Stempel ändert. Du wartest also nicht darauf, dass ein CDN-Cache nachzieht.

Deine Seiten ändern sich nicht. Es gibt keinen Build-Schritt, kein Data-Attribut als Template, kein zweites SDK.

Wenn du den Wert lieber selbst kontrollieren willst, setze window.__CWVREL, bevor der Tracker lädt. Dieser Wert gewinnt dann über den injizierten. Das ist der Weg für Setups, bei denen die App ihre eigene Build-ID kennt und die CI nicht. Beschränke es auf einen echten Versionsstring. Wenn du es an einen Per-Request-Build-Hash koppelst, erhältst du bei jedem Seitenaufruf einen neuen Dimensionswert. Das ruiniert jede gruppierte Abfrage, die du danach ausführst.

Metriken finden ihr Release ausschließlich über dieses Tag. Es gibt keinen Zeitfenster-Fallback, bei dem Core/Dash rät, dass Traffic zwischen zwei Zeitstempeln wahrscheinlich zu einem Release gehört. Wenn ein Release keinen getaggten Traffic hat, zeigt seine Zeile Striche. Das ist die ehrliche Antwort. Sie ist besser als eine Zahl, die die Nutzer der vorherigen Version stillschweigend deiner neuen zuschreibt.

Gib der Sache etwa 200 Seitenaufrufe, bevor du ein Urteil erwartest. Darunter verschiebt sich das p75 durch einzelne Nutzer. Ein Pass oder Fail wäre dann reines Rauschen.

Urteile stammen aus den RUM-Budgets, die du bereits auf der Alerts und Notifications-Seite betreibst. Es gibt kein zweites Set an Grenzwerten, das du für Releases konfigurieren musst. Und auch keine Möglichkeit, dass sich beide widersprechen.

Release-Status

Beide Hälften schreiben in dieselbe Release-Zeile. Eine Version hat also einen Lebenszyklus von geprüft bis ausgeliefert:

StatusGesetzt vonBedeutung
blockedDer Check, bei einem BreachUnter diesem Tag wurde nichts ausgeliefert
pendingDer Check, bei allem anderenBestanden, noch kein Deploy gemeldet
releasedIngest, oder das Formular in der AppLive, mit einer echten Deploy-Zeit

Nur das Erreichen von released stempelt das Projekt ab und purgt das Tracker-Skript. Nicht ausgelieferte Zeilen erscheinen weiterhin auf der Timeline. Sie sind als „Blocked“ oder „Awaiting deploy“ markiert, ohne Deploy-Zeit und ohne echte Nutzer-Metriken. Eine blockierte Zeile enthält die überschrittenen Metriken. So kannst du auch eine Woche später noch sehen, warum diese Version nie live ging.

Das erneute Ausführen des Checks gegen ein bereits ausgeliefertes Tag erfasst das neue Ergebnis. Es ändert weder den Status noch die Deploy-Zeit. Releases, die du vor diesem Feature erfasst hast, haben gar keinen Status und werden als ausgeliefert gewertet.

Das Ganze, End-to-End

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ... build and publish your preview here ...

      - name: Check the preview against budgets
        env:
          COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
        run: |
          STATUS=$(curl -sS -o gate.json -w '%{http_code}' -X POST \
            "https://app.coredash.app/api/project/releases/check" \
            -H "Authorization: Bearer $COREDASH_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{"tag":"${{ github.ref_name }}","origin":"https://preview.example.app","sha":"${{ github.sha }}","runUrl":"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"}')

          if [ "$STATUS" != "200" ]; then
            echo "Gate call failed with HTTP $STATUS: $(cat gate.json)" >&2
            exit 1
          fi

          jq -e '.data.check.status != "breach"' gate.json > /dev/null

      # ... deploy to production here ...

      - name: Report release to CoreDash
        if: success()
        env:
          COREDASH_API_KEY: ${{ secrets.COREDASH_API_KEY }}
        run: |
          curl -sS -f -X POST "https://app.coredash.app/api/project/releases/ingest" \
            -H "Authorization: Bearer ${COREDASH_API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\"tag\":\"${{ github.ref_name }}\",\"sha\":\"${{ github.sha }}\"}"

Nutze dasselbe tag in beiden Aufrufen. Genau das führt das Check-Ergebnis und die echten Nutzer-Metriken in einem Release zusammen statt in zweien.

Was du in Core/Dash siehst

Die Releases-Seite listet die letzten 30 Tage auf, die neuesten zuerst. Pro Release siehst du die Version, ob es aus der CI oder der App kam, die Deploy-Zeit, das p75 für jeden Core Web Vital, das Delta zum vorherigen Release und den Live-Budget-Status. Nicht ausgelieferte Versionen stehen mit ihrem Status-Chip in derselben Liste.

Die Release detail-Ansicht vergleicht das Release unter denselben Filtern mit der gesamten Seite. Oben stehen die Ergebnisse des Pre-Deploy-Checks: eine Zeile pro gescannter Seite pro Budgetgrenze, plus die übersprungenen Metriken. Darunter ein Live-Board jedes RUM-Budgets, das du nicht stummgeschaltet hast.

Performance Snapshots hat einen Releases-Toggle. Er zeichnet Deploy-Marker in die Charts. Marker erscheinen nur für Versionen, die tatsächlich ausgeliefert wurden. Ein blockierter Build taucht also nie als Linie in einem Chart auf, neben Traffic, den er nie bedient hat.

1200x600?text=Releases+list+with+deltas+and+state+chips

Eine Änderung bei den Lighthouse-Budget-Vorschlägen

Vorgeschlagene Lighthouse-Budgets erhalten jetzt 30 % Puffer über dem gemessenen Wert. Vorher waren es 15 %. Auto-Setup und die Crawl-Vorschläge nutzen beide die neue Zahl. Bereits von dir gesetzte Budgets bleiben unangetastet.

Der Grund ist der Pre-Deploy-Check. Ein Budget, das 15 % über dem Wert am Tag der Erstellung liegt, reißt bei der ersten ehrlichen Änderung. Ein Gate, das bei normaler Arbeit anschlägt, wird innerhalb einer Woche abgeschaltet. 30 % lassen Raum für ein echtes Feature und fangen das 4-MB-Bild trotzdem ab.

Wenn ein Release nichts anzeigt

Striche in einer Release-Zeile bedeuten, dass es noch keinen getaggten Traffic gibt. Dafür gibt es nur wenige Gründe. Das Deploy wurde gemeldet, bevor der Code tatsächlich live war. Die eingehenden Seitenaufrufe wurden also noch von der alten Version bedient. Oder das Release wurde vor wenigen Minuten erfasst und es gab noch keine 200 Seitenaufrufe. Oder das von deiner CI gepostete Tag ist nicht das Tag auf den Beacons. Das passiert, wenn eine Seite window.__CWVREL auf etwas anderes setzt und gewinnt.

Gruppiere eine Datentabelle nach der Release-Dimension, um es direkt zu sehen. Wenn dein Tag in dieser Liste auftaucht und Seitenaufrufe dahinter stehen, funktioniert die Zuordnung. Du wartest dann nur noch auf das Volumen.

Siehe auch: Die Core/Dash-API, um diese Daten über ein Skript oder einen KI-Agenten abzufragen. Alerts und Notifications für die Budgets, die diese Urteile nutzen. Und die Installation, falls du den Tracker noch nicht auf deiner Seite hast.