Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
805d2e336f | ||
|
|
7d5582266d
|
||
|
|
51b8608896 | ||
|
|
1521906bca
|
||
|
|
1cd3c0e883 |
No files matched your search
+57
-11
@@ -1,14 +1,60 @@
|
||||
version: 2.1
|
||||
setup: true
|
||||
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.9
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
workflows:
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/^v\d+\.\d+\.\d+$/]
|
||||
@@ -1,7 +0,0 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
jobs:
|
||||
maven:
|
||||
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
|
||||
secrets: inherit
|
||||
@@ -1,23 +0,0 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
@@ -1,74 +1,25 @@
|
||||
# Cut the next release candidate (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6):
|
||||
# run this workflow by hand, or merge a develop PR labelled `release:rc`. It merges develop into
|
||||
# the release line and the release/* push runs CircleCI publish_release_candidate.
|
||||
name: Release candidate
|
||||
'on':
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Plan a cut without changing release state
|
||||
description: Print the plan without merging or creating branches
|
||||
type: boolean
|
||||
default: false
|
||||
finalize:
|
||||
description: Retry finalization of an existing cut; do not cut another RC
|
||||
type: boolean
|
||||
default: false
|
||||
recover:
|
||||
description: Recover the same published RC after a deployment timeout; never
|
||||
reupload
|
||||
type: boolean
|
||||
default: false
|
||||
circle_build:
|
||||
description: Original Circle publisher build number (recovery only)
|
||||
type: string
|
||||
default: ''
|
||||
merge_sha:
|
||||
description: Existing cut merge_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
bump_sha:
|
||||
description: Existing cut bump_sha (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
branch:
|
||||
description: Existing cut branch (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
version:
|
||||
description: Existing cut version (finalize retry only)
|
||||
type: string
|
||||
default: ''
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- develop
|
||||
repository_dispatch:
|
||||
types:
|
||||
- qbit-rc-published
|
||||
types: [closed]
|
||||
branches: [develop]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
checks: read
|
||||
|
||||
jobs:
|
||||
release-rc:
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'repository_dispatch'
|
||||
|| (github.event_name == 'pull_request' && github.event.pull_request.merged
|
||||
== true && github.event.pull_request.base.ref == 'develop' && contains(github.event.pull_request.labels.*.name,
|
||||
'release:rc'))
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-release-rc.yml@cc30f599e63c8e41629c99d23395bac1adb8bc44
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release:rc'))
|
||||
uses: QRun-IO/.github/.github/workflows/qbit-release-rc.yml@main
|
||||
with:
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false
|
||||
}}
|
||||
finalize: ${{ github.event_name == 'repository_dispatch' || (github.event_name
|
||||
== 'workflow_dispatch' && inputs.finalize) }}
|
||||
recover: ${{ github.event_name == 'workflow_dispatch' && inputs.recover || false
|
||||
}}
|
||||
circle_build: ${{ github.event_name == 'workflow_dispatch' && inputs.circle_build
|
||||
|| '' }}
|
||||
merge_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.merge_sha
|
||||
|| inputs.merge_sha || '' }}
|
||||
bump_sha: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.bump_sha
|
||||
|| inputs.bump_sha || '' }}
|
||||
branch: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.branch
|
||||
|| inputs.branch || '' }}
|
||||
version: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.version
|
||||
|| inputs.version || '' }}
|
||||
secrets: inherit
|
||||
dry_run: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: non-release
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
asciidoc: false
|
||||
release_completion: true
|
||||
+1
-8
@@ -4,18 +4,12 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Changed
|
||||
- The Performance home is business-only. The provider Setup readout and Recent Runs left the home's widget list (`WebPerfWidgets.HOME`); they are `WebPerfWidgets.ADMIN` (`Setup`, `RecentRuns`), gated to `<base>.admin.hasAccess` and hidden when denied, for a host to place on its Admin area. A new `NotConfigured` widget leads the home: while no Google API key is set it says "Page speed testing is not set up. Ask an admin to add a Google PageSpeed API key." (no config key names), and renders nothing once configured.
|
||||
|
||||
### Fixed
|
||||
- Read the QBit metadata version from the build so release artifacts report their actual version, matching the help bundle.
|
||||
- Use dropdowns for tier, scope, source, strategy, metric, comparator and severity, preserving existing stored values.
|
||||
- Allow manual Tested Pages inserts with a URL. Validate absolute HTTPS URLs and fill in the site, path and Manual source automatically; validate URL edits too.
|
||||
|
||||
### Added
|
||||
- Ship the host-neutral `help/qbit-web-performance.yaml` QBIT bundle, register it with QQQ help bundles, and enforce metadata coverage and live-reference validity with `WebPerfHelpBundleTest`. Keep app and table empty-state help available without Help installed.
|
||||
- Foreign keys for every reference between the QBit's tables (changesets 12-26): findings and reports cascade with their run, comparisons with their site change; optional pointers (likely cause, first and confirmation run, a run's site change) are set to NULL; tested pages and budgets with history cannot be deleted (disable them instead). Rows whose parent is already missing are recorded in `qqq_fk_quarantine`, then nulled or (NOT NULL columns, PostgreSQL) deleted, so the upgrade never fails. Foreign key columns are indexed. A PostgreSQL guardrail test fails when a new reference lacks its constraint.
|
||||
- Every `web_performance_*` table and the Performance app carry help saying what the table is for and how its rows get created (`help/qbit-web-performance.yaml`, applied by `WebPerfHelp`; host help is never overridden). A Setup card at the top of the home lists tested pages, the lab runner, budgets (reference defaults or your own) and Google field data, each with its state and next step, using the runtime's own resolution. `WebPerfTablesGuardrailTest` fails when a table is unused, unexplained or lacks that help; `WebPerfDefaultsChangelogTest` runs the shipped changelog and its seeded budgets through real Liquibase.
|
||||
- Every `web_performance_*` table and the Performance app carry help saying what the table is for and how its rows get created (`help/web-performance.yaml`, applied by `WebPerfHelp`; host help is never overridden). A Setup card at the top of the home lists tested pages, the lab runner, budgets (reference defaults or your own) and Google field data, each with its state and next step, using the runtime's own resolution. `WebPerfTablesGuardrailTest` fails when a table is unused, unexplained or lacks that help; `WebPerfDefaultsChangelogTest` runs the shipped changelog and its seeded budgets through real Liquibase.
|
||||
- `reportRetentionDays` is now enforced: with a value above 0, Lighthouse Reports older than that many days are deleted after a result is stored, in batches, at most once an hour per process (runs, scores and findings stay). It was documented but nothing read it; the default 0 still keeps everything.
|
||||
- Record deploy and external site changes once, queue settled retests, and attribute lab runs to the observed build and CDN stage.
|
||||
- Evaluate enabled lab and URL CrUX budgets; retain breaches, confirmation evidence, automatic recovery and accurate open counts in the standard regression records.
|
||||
@@ -25,7 +19,6 @@ All notable changes to this project are documented here. The format follows Keep
|
||||
- Raise and resolve optional alerts through QQQ core, with logging fallback when no Alerts QBit is installed.
|
||||
|
||||
### Changed
|
||||
- Pin QQQ 4.1.0-RC.7 for the released help-bundle API.
|
||||
- Use QQQ 4.1.0-RC.6 for the core alerts API; add migration fields for durable evaluation and source-event identity.
|
||||
- Store complete run results atomically and retain confirmation/comparison identities when retrying samples.
|
||||
|
||||
|
||||
@@ -70,19 +70,7 @@ Tier, Scope, Source, Strategy, Metric, Comparator and Severity use dropdowns wit
|
||||
|
||||
### Starter defaults and empty-state help
|
||||
|
||||
The changelog's changeset 10 seeds 13 reference budgets (Google's "good" thresholds and the Lighthouse category bands); nothing else is seeded. The target table is filled by Sync pages from sitemap, and the lab, finding, report, field, regression, change and comparison tables are filled by the qbit as it runs (empty is a normal start). Every table's help (`help/qbit-web-performance.yaml`, shown on the table screen) says how its rows get created, and the Setup card on the home shows what still needs doing. Budgets with source RUM are not evaluated yet (no real-user source is wired); LAB and CRUX budgets are.
|
||||
|
||||
## Scheduled audit quota
|
||||
|
||||
Set `withMaxRunsPerDay(300)` in the host configuration to reserve headroom below the default 500-run cap. Key-page schedules, full audits and manually queued runs share that daily allowance in the configured time zone. Scheduled requests prioritize page/device pairs never queued before, then those queued least recently. Quota-skipped rows do not advance that rotation, so large sitemaps continue through the remaining pairs on later days instead of repeatedly testing the first pages. Existing queued/running pairs still merge; manual selection keeps its requested order.
|
||||
|
||||
## In-app help
|
||||
|
||||
The QBit registers [`help/qbit-web-performance.yaml`](src/main/resources/help/qbit-web-performance.yaml) with QQQ help bundles (RC.7 or later). Registration is metadata-only; core skips startup synchronization when the host has no `helpContent` table. App and table empty-state help remains available without Help installed.
|
||||
|
||||
To override text, register your own uniquely named `layer: HOST` bundle with entries using the same keys and roles. HOST entries take precedence over QBIT entries; startup synchronization preserves administrator edits. Keep deployment-specific URLs, schedules and content-table actions in the host bundle. The shipped keys use the default metadata names; hosts that rename or namespace objects should provide help matching those names.
|
||||
|
||||
`WebPerfHelpBundleTest` checks owned metadata, live references and summary length with `HelpBundleAssertions`. Run it as part of `./mvnw verify`. The bundle version is the literal Maven project version; update it when changing the project version.
|
||||
The changelog's changeset 10 seeds 13 reference budgets (Google's "good" thresholds and the Lighthouse category bands); nothing else is seeded. The target table is filled by Sync pages from sitemap, and the lab, finding, report, field, regression, change and comparison tables are filled by the qbit as it runs (empty is a normal start). Every table's help (`help/web-performance.yaml`, shown on the table screen) says how its rows get created, and the Setup card on the home shows what still needs doing. Budgets with source RUM are not evaluated yet (no real-user source is wired); LAB and CRUX budgets are.
|
||||
|
||||
## Permissions
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Upgrading
|
||||
|
||||
## Business-only home; admin widgets (unreleased)
|
||||
|
||||
No DDL change. The Setup and Recent Runs widgets are no longer on the Performance home. They are still registered (`webPerformanceSetup`, `webPerformanceRecentRuns`), gated to `web_performance.admin.hasAccess` and hidden without it; add `WebPerfWidgets.ADMIN` (qualified with `config.qualify`) to your Admin app's widgets to keep them. The home gains a `webPerformanceNotConfigured` widget that shows only while no Google API key is set.
|
||||
|
||||
## Table help, Setup card and `reportRetentionDays` (unreleased)
|
||||
|
||||
No DDL change, no new changeset and no host include change. The qbit now fills empty `help` slots on its app and tables (help a host set is kept) and adds a Setup widget to the Performance home. `reportRetentionDays` (default 0, keep forever) is now enforced: set it above 0 only if you want Lighthouse Reports older than that many days deleted.
|
||||
@@ -18,19 +18,16 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<qqq.version>4.1.0-RC.26</qqq.version> <!-- feeds qbit-build-parent's qqq-bom-pom import -->
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<revision>0.1.0-RC.3</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
<liquibase.version>4.33.0</liquibase.version>
|
||||
<testcontainers.version>1.21.4</testcontainers.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>com.kingsrook</groupId>
|
||||
<artifactId>qbit-build-parent</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<version>2.0.0</version>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kingsrook.qbits</groupId>
|
||||
@@ -103,63 +100,10 @@
|
||||
<version>3.27.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- The foreign key guardrail runs the changelog with Liquibase on PostgreSQL (Docker). -->
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
<version>${liquibase.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.14</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${testcontainers.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<!-- help bundles carry the artifact version (QQQ help bundle provenance) -->
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>help/*.yaml</include>
|
||||
<include>qbit-web-performance.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>false</filtering>
|
||||
<excludes>
|
||||
<exclude>help/*.yaml</exclude>
|
||||
<exclude>qbit-web-performance.properties</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables combine.children="append">
|
||||
<qbit.artifactVersion>${project.version}</qbit.artifactVersion>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
@@ -206,14 +150,14 @@
|
||||
<profiles>
|
||||
<!-- Built against qqq 4.1 (QBitMetaDataProducer). Until qbit-build-parent -->
|
||||
<!-- imports the released 4.1 BOM, this profile is active by default and -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.7 ahead of the parent's 4.0.0 BOM. -->
|
||||
<!-- imports qqq-bom-pom 4.1.0-RC.6 ahead of the parent's 4.0.0 BOM. -->
|
||||
<profile>
|
||||
<id>qqq-snapshot</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<qqq.snapshot.version>4.1.0-RC.26</qqq.snapshot.version>
|
||||
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
||||
@@ -21,17 +21,13 @@
|
||||
package com.kingsrook.qbits.webperf;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import com.kingsrook.qbits.webperf.metadata.WebPerfMetaData;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
@@ -65,41 +61,13 @@ public class WebPerfQBitProducer implements QBitMetaDataProducer<WebPerfQBitConf
|
||||
|
||||
public static final String GROUP_ID = "com.kingsrook.qbits";
|
||||
public static final String ARTIFACT_ID = "web-performance";
|
||||
public static final String VERSION = loadVersion();
|
||||
public static final String VERSION = "0.1.0";
|
||||
|
||||
private WebPerfQBitConfig qBitConfig;
|
||||
private String namespace;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Read the artifact version from the Maven-filtered properties resource.
|
||||
*******************************************************************************/
|
||||
private static String loadVersion()
|
||||
{
|
||||
try(InputStream inputStream = WebPerfQBitProducer.class.getClassLoader().getResourceAsStream("qbit-web-performance.properties"))
|
||||
{
|
||||
if(inputStream == null)
|
||||
{
|
||||
throw (new IllegalStateException("Missing qbit-web-performance.properties; build with Maven to generate the artifact version"));
|
||||
}
|
||||
Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
String version = properties.getProperty("version");
|
||||
if(version == null || version.isBlank() || version.contains("${"))
|
||||
{
|
||||
throw (new IllegalStateException("Missing or unfiltered version in qbit-web-performance.properties; build with Maven"));
|
||||
}
|
||||
return (version);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw (new IllegalStateException("Could not read artifact version from qbit-web-performance.properties", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for the config.
|
||||
**
|
||||
@@ -224,8 +192,6 @@ public class WebPerfQBitProducer implements QBitMetaDataProducer<WebPerfQBitConf
|
||||
new WebPerfRuntime(qBitConfig).addSelfToInstance(qInstance);
|
||||
new WebPerfMetaData(qBitConfig).addTo(qInstance);
|
||||
com.kingsrook.qbits.webperf.metadata.WebPerfHelp.apply(qInstance, qBitConfig);
|
||||
// Registration is metadata-only; core guards startup sync when helpContent is absent.
|
||||
HelpBundles.register(qInstance, "help/qbit-web-performance.yaml");
|
||||
|
||||
LOG.info("Web performance registered", logPair("tablePrefix", qBitConfig.getTableNamePrefix()), logPair("app", qBitConfig.getAppName()),
|
||||
logPair("siteBaseUrl", qBitConfig.getSiteBaseUrl()), logPair("features", qBitConfig.getFeatures()),
|
||||
|
||||
@@ -33,15 +33,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Applies help/qbit-web-performance.yaml (the QQQ help-bundle format) to the
|
||||
** Applies help/web-performance.yaml (the QQQ help-bundle format) to the
|
||||
** Performance app and its web_performance_* tables: what each table is for and
|
||||
** how its rows get created, shown on the table screen and when it is empty.
|
||||
** Keeps table and app empty-state help available even without the Help tables.
|
||||
** The producer also registers this resource with the core bundle registry.
|
||||
** STAND-IN(core=QRun-IO/qqq#1198, removeBy=4.1.0-RC.9): replace with HelpBundles.register once the pinned QQQ
|
||||
** release includes it; the bundle file stays the same.
|
||||
*******************************************************************************/
|
||||
public final class WebPerfHelp
|
||||
{
|
||||
public static final String RESOURCE = "/help/qbit-web-performance.yaml";
|
||||
public static final String RESOURCE = "/help/web-performance.yaml";
|
||||
public static final String SLOT = "help";
|
||||
|
||||
|
||||
@@ -67,18 +67,14 @@ public final class WebPerfHelp
|
||||
throw (new IOException("missing resource"));
|
||||
}
|
||||
JsonNode bundle = new ObjectMapper(new YAMLFactory()).readTree(stream);
|
||||
if(!"qbit-web-performance".equals(bundle.path("bundle").asText()) || !"QBIT".equals(bundle.path("layer").asText()))
|
||||
if(!"web-performance".equals(bundle.path("bundle").asText()) || !"QBIT".equals(bundle.path("layer").asText()))
|
||||
{
|
||||
throw (new IOException("invalid bundle header"));
|
||||
}
|
||||
Map<String, JsonNode> entries = new LinkedHashMap<>();
|
||||
for(JsonNode entry : bundle.path("entries"))
|
||||
{
|
||||
String key = entry.path("key").asText();
|
||||
if(!key.endsWith(";slot:help") || !(key.startsWith("table:") || key.startsWith("app:")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = entry.path("key").asText();
|
||||
String summary = entry.path("summary").asText();
|
||||
if(key.isBlank() || summary.isBlank() || summary.length() > 120 || entry.path("content").asText().isBlank() || entries.put(key, entry) != null)
|
||||
{
|
||||
|
||||
@@ -50,7 +50,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
@@ -130,15 +129,13 @@ public final class WebPerfMetaData
|
||||
{
|
||||
qInstance.addProcess(process);
|
||||
}
|
||||
addWidget(qInstance, WebPerfWidgets.NOT_CONFIGURED, "Page Speed Setup", WidgetType.HTML, WebPerfWidgets.NotConfigured.class);
|
||||
qInstance.getWidget(config.qualify(WebPerfWidgets.NOT_CONFIGURED)).setIsCard(false);
|
||||
addAdminWidget(qInstance, WebPerfWidgets.SETUP, "Page Speed Setup (Admin)", WebPerfWidgets.Setup.class);
|
||||
addWidget(qInstance, WebPerfWidgets.SETUP, "Setup", WidgetType.HTML, WebPerfWidgets.Setup.class);
|
||||
addWidget(qInstance, WebPerfWidgets.SITE_SCORES, "Site Scores", WidgetType.HTML, WebPerfWidgets.SiteScores.class);
|
||||
addWidget(qInstance, WebPerfWidgets.TREND, "Performance Trend", WidgetType.LINE_CHART, WebPerfWidgets.Trend.class);
|
||||
addWidget(qInstance, WebPerfWidgets.SLOWEST_PAGES, "Slowest Pages", WidgetType.HTML, WebPerfWidgets.SlowestPages.class);
|
||||
addWidget(qInstance, WebPerfWidgets.OPPORTUNITIES, "Top Opportunities", WidgetType.HTML, WebPerfWidgets.TopOpportunities.class);
|
||||
addWidget(qInstance, WebPerfWidgets.FIELD_STATUS, "Google Field Data", WidgetType.HTML, WebPerfWidgets.FieldDataStatus.class);
|
||||
addAdminWidget(qInstance, WebPerfWidgets.RECENT_RUNS, "Recent Runs", WebPerfWidgets.RecentRuns.class);
|
||||
addWidget(qInstance, WebPerfWidgets.RECENT_RUNS, "Recent Runs", WidgetType.HTML, WebPerfWidgets.RecentRuns.class);
|
||||
addWidget(qInstance, WebPerfWidgets.RECORD_PANEL, "Performance", WidgetType.HTML, WebPerfWidgets.RecordPanel.class);
|
||||
|
||||
List<String> panelTables = new ArrayList<>(config.getContentTables());
|
||||
@@ -261,20 +258,6 @@ public final class WebPerfMetaData
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A system widget for a host's Admin area (WebPerfWidgets.ADMIN): gated to
|
||||
** "<base>.admin.hasAccess" and HIDDEN, never a locked shell, when denied.
|
||||
*******************************************************************************/
|
||||
private void addAdminWidget(QInstance qInstance, String name, String label, Class<? extends AbstractWidgetRenderer> renderer)
|
||||
{
|
||||
addWidget(qInstance, name, label, WidgetType.HTML, renderer);
|
||||
qInstance.getWidget(config.qualify(name)).setPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
|
||||
.withPermissionBaseName(config.getPermissionBaseName() + ".admin")
|
||||
.withDenyBehavior(DenyBehavior.HIDDEN));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Performance app: its one home (the widgets) and the data tables.
|
||||
** No process is in its nav.
|
||||
|
||||
@@ -209,8 +209,7 @@ public final class ScheduledSteps
|
||||
try
|
||||
{
|
||||
WebPerfService service = WebPerfRuntime.forInput(input).service();
|
||||
Instant now = Instant.now();
|
||||
WebPerfService.QueuePlan plan = service.enqueue(service.scheduledRequests(keyOnly, reason, now), now);
|
||||
WebPerfService.QueuePlan plan = service.enqueue(service.requestsFor(service.enabledTargets(keyOnly), "SCHEDULE", reason, Instant.now()), Instant.now());
|
||||
LOG.info("Web performance tests queued", logPair("reason", reason), logPair("queued", plan.queue().size()), logPair("merged", plan.merged().size()),
|
||||
logPair("overCap", plan.overQuota().size()));
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -450,32 +448,6 @@ public class WebPerfService
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Scheduled requests, least recently queued page/strategy first. Quota skips
|
||||
** do not advance the rotation; never-tested pairs precede historical runs.
|
||||
** Manual requests keep the caller's ordering.
|
||||
**
|
||||
** @param keyOnly only KEY pages
|
||||
** @param reason schedule description
|
||||
** @param now earliest start
|
||||
** @return requests ordered fairly across daily quota boundaries
|
||||
** @throws SQLException on a history read failure
|
||||
*******************************************************************************/
|
||||
public List<RunRequest> scheduledRequests(boolean keyOnly, String reason, Instant now) throws SQLException
|
||||
{
|
||||
List<RunRequest> requests = requestsFor(enabledTargets(keyOnly), "SCHEDULE", reason, now);
|
||||
Map<String, Instant> lastQueued = new HashMap<>();
|
||||
for(Map<String, Object> row : store.query("SELECT r.target_id, r.strategy, MAX(r.queued_at) AS last_queued FROM {lab_run} r JOIN {target} t ON t.id = r.target_id WHERE t.site_key = ? AND r.status <> 'SKIPPED_QUOTA' GROUP BY r.target_id, r.strategy", siteKey()))
|
||||
{
|
||||
lastQueued.put(WebPerfStore.asLong(row.get("target_id")) + "|" + row.get("strategy"), (Instant) row.get("last_queued"));
|
||||
}
|
||||
requests.sort(Comparator.comparing(request -> lastQueued.get(request.targetId() + "|" + request.strategy()),
|
||||
Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
return (requests);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enabled pages, optionally only the KEY tier.
|
||||
**
|
||||
|
||||
@@ -59,7 +59,6 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
|
||||
public final class WebPerfWidgets
|
||||
{
|
||||
public static final String SETUP = "Setup";
|
||||
public static final String NOT_CONFIGURED = "NotConfigured";
|
||||
public static final String SITE_SCORES = "SiteScores";
|
||||
public static final String TREND = "Trend";
|
||||
public static final String SLOWEST_PAGES = "SlowestPages";
|
||||
@@ -68,18 +67,8 @@ public final class WebPerfWidgets
|
||||
public static final String FIELD_STATUS = "FieldDataStatus";
|
||||
public static final String RECORD_PANEL = "RecordPanel";
|
||||
|
||||
/** The business home's widgets, in order. No provider or run-queue detail (standard A-0). */
|
||||
public static final List<String> HOME = List.of(NOT_CONFIGURED, SITE_SCORES, TREND, SLOWEST_PAGES, OPPORTUNITIES, FIELD_STATUS);
|
||||
|
||||
/**
|
||||
* The system widgets (provider setup readout, run queue). Not on the home: a host places them on its
|
||||
* Admin area. Each is gated to "<base>.admin.hasAccess" and hidden (not shown locked) without it.
|
||||
*/
|
||||
public static final List<String> ADMIN = List.of(SETUP, RECENT_RUNS);
|
||||
|
||||
/** The business empty state shown on the home while no Google API key is set. */
|
||||
public static final String NOT_CONFIGURED_TITLE = "Page speed testing is not set up";
|
||||
public static final String NOT_CONFIGURED_BODY = "Ask an admin to add a Google PageSpeed API key.";
|
||||
/** The home widgets, in order. */
|
||||
public static final List<String> HOME = List.of(SETUP, SITE_SCORES, TREND, SLOWEST_PAGES, OPPORTUNITIES, FIELD_STATUS, RECENT_RUNS);
|
||||
|
||||
static final int TREND_WEEKS = 26;
|
||||
|
||||
@@ -427,29 +416,6 @@ public final class WebPerfWidgets
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Not Configured: the home's business empty state. Shown only while lab tests
|
||||
** cannot run because no Google API key is set; says who can fix it and names
|
||||
** no configuration key. Renders nothing once the provider is configured.
|
||||
*******************************************************************************/
|
||||
public static class NotConfigured extends AbstractWidgetRenderer
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Render.
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
if(runtime(input).labRunner() != null)
|
||||
{
|
||||
return (html(""));
|
||||
}
|
||||
return (html("<div style=\"font-weight:600\">" + Html.escape(NOT_CONFIGURED_TITLE) + "</div>" + Html.muted(NOT_CONFIGURED_BODY)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setup: what is configured (pages, lab runner, budgets, field data) and
|
||||
** the next step for each (WebPerfSetupStatus).
|
||||
|
||||
@@ -280,154 +280,3 @@ ALTER TABLE ${web_performance.tablePrefix}regression ADD COLUMN failed_audits VA
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_run ADD COLUMN ttfb_ms INTEGER;
|
||||
ALTER TABLE ${web_performance.tablePrefix}field_snapshot ADD COLUMN evaluated_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
ALTER TABLE ${web_performance.tablePrefix}change_event ADD COLUMN last_seen_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
|
||||
-- changeset qbit-web-performance:12
|
||||
-- comment: Referential integrity (foreign keys), part 1: every foreign key column leads an index, so a parent delete checks its children by index. The other foreign key columns already lead one (target_id, run_id, change_event_id on comparison).
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}lab_run_change_event ON ${web_performance.tablePrefix}lab_run (change_event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}regression_budget ON ${web_performance.tablePrefix}regression (budget_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}regression_cause ON ${web_performance.tablePrefix}regression (cause_change_event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}regression_first_run ON ${web_performance.tablePrefix}regression (first_run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}regression_checkpoint_run ON ${web_performance.tablePrefix}regression (checkpoint_run_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_${web_performance.tablePrefix}comparison_target ON ${web_performance.tablePrefix}comparison (target_id);
|
||||
|
||||
-- Referential integrity (foreign keys), part 2: one changeset per foreign key, named
|
||||
-- fk_<child table>_<column>. Each first records and repairs rows whose parent is
|
||||
-- missing (a nullable column is set to NULL; a NOT NULL row is copied in full to
|
||||
-- qqq_fk_quarantine and deleted, PostgreSQL only), then adds the constraint. Each
|
||||
-- is marked ran when its constraint already exists (a host that added it itself).
|
||||
-- ON DELETE: owned details CASCADE (findings and the report of a run,
|
||||
-- comparisons of a site change); optional pointers SET NULL; tested pages and
|
||||
-- budgets RESTRICT, because history is kept (D8): disable a page or budget instead
|
||||
-- of deleting it.
|
||||
|
||||
-- changeset qbit-web-performance:13
|
||||
-- comment: Lab runs keep their tested page (D8, history is kept): a page with runs cannot be deleted, disable it instead (enabled = false). Orphan target_id values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_run_target_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}lab_run', 'target_id', CAST(c.id AS VARCHAR(100)), CAST(c.target_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:13' FROM ${web_performance.tablePrefix}lab_run c WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
UPDATE ${web_performance.tablePrefix}lab_run c SET target_id = NULL WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_run ADD CONSTRAINT fk_${web_performance.tablePrefix}lab_run_target_id FOREIGN KEY (target_id) REFERENCES ${web_performance.tablePrefix}target (id) ON DELETE RESTRICT;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}lab_run DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}lab_run_target_id;
|
||||
|
||||
-- changeset qbit-web-performance:14 dbms:postgresql
|
||||
-- comment: Orphan cleanup before fk_${web_performance.tablePrefix}lab_finding_run_id: ${web_performance.tablePrefix}lab_finding rows whose run_id names a missing ${web_performance.tablePrefix}lab_run row are copied in full to qqq_fk_quarantine, then deleted (run_id is NOT NULL).
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_finding_run_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, row_data, changeset) SELECT '${web_performance.tablePrefix}lab_finding', 'run_id', CAST(c.id AS VARCHAR(100)), CAST(c.run_id AS VARCHAR(100)), 'DELETED', row_to_json(c)::text, 'qbit-web-performance:14' FROM ${web_performance.tablePrefix}lab_finding c WHERE c.run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.run_id);
|
||||
DELETE FROM ${web_performance.tablePrefix}lab_finding c WHERE c.run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.run_id);
|
||||
-- rollback SELECT 1;
|
||||
|
||||
-- changeset qbit-web-performance:15
|
||||
-- comment: Findings belong to their run: deleting a run deletes its findings.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_finding_run_id'
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_finding ADD CONSTRAINT fk_${web_performance.tablePrefix}lab_finding_run_id FOREIGN KEY (run_id) REFERENCES ${web_performance.tablePrefix}lab_run (id) ON DELETE CASCADE;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}lab_finding DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}lab_finding_run_id;
|
||||
|
||||
-- changeset qbit-web-performance:16
|
||||
-- comment: A finding's page (copied from its run) is kept like the run's. Orphan target_id values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_finding_target_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}lab_finding', 'target_id', CAST(c.id AS VARCHAR(100)), CAST(c.target_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:16' FROM ${web_performance.tablePrefix}lab_finding c WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
UPDATE ${web_performance.tablePrefix}lab_finding c SET target_id = NULL WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_finding ADD CONSTRAINT fk_${web_performance.tablePrefix}lab_finding_target_id FOREIGN KEY (target_id) REFERENCES ${web_performance.tablePrefix}target (id) ON DELETE RESTRICT;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}lab_finding DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}lab_finding_target_id;
|
||||
|
||||
-- changeset qbit-web-performance:17 dbms:postgresql
|
||||
-- comment: Orphan cleanup before fk_${web_performance.tablePrefix}lab_report_run_id: ${web_performance.tablePrefix}lab_report rows whose run_id names a missing ${web_performance.tablePrefix}lab_run row are copied in full to qqq_fk_quarantine, then deleted (run_id is NOT NULL).
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_report_run_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, row_data, changeset) SELECT '${web_performance.tablePrefix}lab_report', 'run_id', CAST(c.id AS VARCHAR(100)), CAST(c.run_id AS VARCHAR(100)), 'DELETED', row_to_json(c)::text, 'qbit-web-performance:17' FROM ${web_performance.tablePrefix}lab_report c WHERE c.run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.run_id);
|
||||
DELETE FROM ${web_performance.tablePrefix}lab_report c WHERE c.run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.run_id);
|
||||
-- rollback SELECT 1;
|
||||
|
||||
-- changeset qbit-web-performance:18
|
||||
-- comment: The Lighthouse report belongs to its run: deleting a run deletes its report.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_report_run_id'
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_report ADD CONSTRAINT fk_${web_performance.tablePrefix}lab_report_run_id FOREIGN KEY (run_id) REFERENCES ${web_performance.tablePrefix}lab_run (id) ON DELETE CASCADE;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}lab_report DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}lab_report_run_id;
|
||||
|
||||
-- changeset qbit-web-performance:19
|
||||
-- comment: Regressions keep their page: a page with regressions cannot be deleted, disable it instead. Orphan target_id values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}regression_target_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}regression', 'target_id', CAST(c.id AS VARCHAR(100)), CAST(c.target_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:19' FROM ${web_performance.tablePrefix}regression c WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
UPDATE ${web_performance.tablePrefix}regression c SET target_id = NULL WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}regression ADD CONSTRAINT fk_${web_performance.tablePrefix}regression_target_id FOREIGN KEY (target_id) REFERENCES ${web_performance.tablePrefix}target (id) ON DELETE RESTRICT;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}regression DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}regression_target_id;
|
||||
|
||||
-- changeset qbit-web-performance:20
|
||||
-- comment: A regression keeps the budget it breached: a budget with regressions cannot be deleted, disable it instead (enabled = false). Orphan budget_id values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}regression_budget_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}regression', 'budget_id', CAST(c.id AS VARCHAR(100)), CAST(c.budget_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:20' FROM ${web_performance.tablePrefix}regression c WHERE c.budget_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}budget p WHERE p.id = c.budget_id);
|
||||
UPDATE ${web_performance.tablePrefix}regression c SET budget_id = NULL WHERE c.budget_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}budget p WHERE p.id = c.budget_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}regression ADD CONSTRAINT fk_${web_performance.tablePrefix}regression_budget_id FOREIGN KEY (budget_id) REFERENCES ${web_performance.tablePrefix}budget (id) ON DELETE RESTRICT;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}regression DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}regression_budget_id;
|
||||
|
||||
-- changeset qbit-web-performance:21
|
||||
-- comment: The likely cause is an optional pointer: deleting the site change clears it. Orphan values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}regression_cause_change_event_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}regression', 'cause_change_event_id', CAST(c.id AS VARCHAR(100)), CAST(c.cause_change_event_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:21' FROM ${web_performance.tablePrefix}regression c WHERE c.cause_change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.cause_change_event_id);
|
||||
UPDATE ${web_performance.tablePrefix}regression c SET cause_change_event_id = NULL WHERE c.cause_change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.cause_change_event_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}regression ADD CONSTRAINT fk_${web_performance.tablePrefix}regression_cause_change_event_id FOREIGN KEY (cause_change_event_id) REFERENCES ${web_performance.tablePrefix}change_event (id) ON DELETE SET NULL;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}regression DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}regression_cause_change_event_id;
|
||||
|
||||
-- changeset qbit-web-performance:22
|
||||
-- comment: A before-and-after comparison is computed for its site change: deleting the change deletes its comparisons. Orphan values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}comparison_change_event_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}comparison', 'change_event_id', CAST(c.id AS VARCHAR(100)), CAST(c.change_event_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:22' FROM ${web_performance.tablePrefix}comparison c WHERE c.change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.change_event_id);
|
||||
UPDATE ${web_performance.tablePrefix}comparison c SET change_event_id = NULL WHERE c.change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.change_event_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}comparison ADD CONSTRAINT fk_${web_performance.tablePrefix}comparison_change_event_id FOREIGN KEY (change_event_id) REFERENCES ${web_performance.tablePrefix}change_event (id) ON DELETE CASCADE;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}comparison DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}comparison_change_event_id;
|
||||
|
||||
-- changeset qbit-web-performance:23
|
||||
-- comment: The site change a run was attributed to is an optional pointer: deleting the change clears it. Orphan values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}lab_run_change_event_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}lab_run', 'change_event_id', CAST(c.id AS VARCHAR(100)), CAST(c.change_event_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:23' FROM ${web_performance.tablePrefix}lab_run c WHERE c.change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.change_event_id);
|
||||
UPDATE ${web_performance.tablePrefix}lab_run c SET change_event_id = NULL WHERE c.change_event_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}change_event p WHERE p.id = c.change_event_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}lab_run ADD CONSTRAINT fk_${web_performance.tablePrefix}lab_run_change_event_id FOREIGN KEY (change_event_id) REFERENCES ${web_performance.tablePrefix}change_event (id) ON DELETE SET NULL;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}lab_run DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}lab_run_change_event_id;
|
||||
|
||||
-- changeset qbit-web-performance:24
|
||||
-- comment: Evidence pointer to the run that first showed the regression: deleting the run clears it. Orphan values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}regression_first_run_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}regression', 'first_run_id', CAST(c.id AS VARCHAR(100)), CAST(c.first_run_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:24' FROM ${web_performance.tablePrefix}regression c WHERE c.first_run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.first_run_id);
|
||||
UPDATE ${web_performance.tablePrefix}regression c SET first_run_id = NULL WHERE c.first_run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.first_run_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}regression ADD CONSTRAINT fk_${web_performance.tablePrefix}regression_first_run_id FOREIGN KEY (first_run_id) REFERENCES ${web_performance.tablePrefix}lab_run (id) ON DELETE SET NULL;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}regression DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}regression_first_run_id;
|
||||
|
||||
-- changeset qbit-web-performance:25
|
||||
-- comment: Evidence pointer to the confirmation run: deleting the run clears it. Orphan values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}regression_checkpoint_run_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}regression', 'checkpoint_run_id', CAST(c.id AS VARCHAR(100)), CAST(c.checkpoint_run_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:25' FROM ${web_performance.tablePrefix}regression c WHERE c.checkpoint_run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.checkpoint_run_id);
|
||||
UPDATE ${web_performance.tablePrefix}regression c SET checkpoint_run_id = NULL WHERE c.checkpoint_run_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}lab_run p WHERE p.id = c.checkpoint_run_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}regression ADD CONSTRAINT fk_${web_performance.tablePrefix}regression_checkpoint_run_id FOREIGN KEY (checkpoint_run_id) REFERENCES ${web_performance.tablePrefix}lab_run (id) ON DELETE SET NULL;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}regression DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}regression_checkpoint_run_id;
|
||||
|
||||
-- changeset qbit-web-performance:26
|
||||
-- comment: A per-page comparison keeps its page: a page with comparisons cannot be deleted, disable it instead. Orphan target_id values are recorded in qqq_fk_quarantine and set to NULL.
|
||||
-- preconditions onFail:MARK_RAN onError:HALT
|
||||
-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.table_constraints WHERE LOWER(constraint_name) = 'fk_${web_performance.tablePrefix}comparison_target_id'
|
||||
CREATE TABLE IF NOT EXISTS qqq_fk_quarantine (id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, table_name VARCHAR(100) NOT NULL, column_name VARCHAR(100) NOT NULL, row_id VARCHAR(100), missing_parent_id VARCHAR(100), action VARCHAR(20) NOT NULL, row_data TEXT, changeset VARCHAR(200), quarantined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||
INSERT INTO qqq_fk_quarantine (table_name, column_name, row_id, missing_parent_id, action, changeset) SELECT '${web_performance.tablePrefix}comparison', 'target_id', CAST(c.id AS VARCHAR(100)), CAST(c.target_id AS VARCHAR(100)), 'NULLED', 'qbit-web-performance:26' FROM ${web_performance.tablePrefix}comparison c WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
UPDATE ${web_performance.tablePrefix}comparison c SET target_id = NULL WHERE c.target_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${web_performance.tablePrefix}target p WHERE p.id = c.target_id);
|
||||
ALTER TABLE ${web_performance.tablePrefix}comparison ADD CONSTRAINT fk_${web_performance.tablePrefix}comparison_target_id FOREIGN KEY (target_id) REFERENCES ${web_performance.tablePrefix}target (id) ON DELETE RESTRICT;
|
||||
-- rollback ALTER TABLE ${web_performance.tablePrefix}comparison DROP CONSTRAINT IF EXISTS fk_${web_performance.tablePrefix}comparison_target_id;
|
||||
@@ -1,666 +0,0 @@
|
||||
bundle: qbit-web-performance
|
||||
version: ${project.version}
|
||||
layer: QBIT
|
||||
entries:
|
||||
- key: app:webPerformance;slot:help
|
||||
summary: Page speed tests, Google field data, budgets and regressions for your site.
|
||||
content: |
|
||||
Sync your pages and review your budgets, then run tests. The dashboard shows site scores, trends, slow pages, opportunities and field data. If page speed testing is not set up, it says so and tells you to ask an admin.
|
||||
|
||||
Run **{{label:webPerformanceTestPageSpeed}}** for selected pages or **{{label:webPerformanceRunSiteAudit}}** for all enabled pages. Mobile and desktop are separate runs by default. Your host controls the enabled features, device strategies, daily run limit and scheduler. Without a configured runner, lab tests wait in the queue.
|
||||
|
||||
Review **{{label:web_performance_regression}}** for budget breaches and performance changes. Testing requires the testing permission; syncing pages and refreshing field data require administrator permission.
|
||||
- key: table:web_performance_target;slot:help
|
||||
summary: Every public page that is tested for speed, with its latest scores.
|
||||
content: |
|
||||
**How rows get created:** Sync pages from your URL source, or add an HTTPS URL with New.
|
||||
|
||||
Use this list to choose which pages you test and review their latest scores. **{{label:webPerformanceSyncPages}}** imports URLs from your configured URL source. Pages that disappear from the source are retained with their history. You can also add an HTTPS URL with **New**.
|
||||
|
||||
Edit the URL, friendly label, tier or enabled setting as needed. Disable a page to skip it when queuing tests. Mark important pages as key pages to include them in key-page tests and site scores.
|
||||
|
||||
Use **{{label:webPerformanceTestPageSpeed}}** for selected pages or **{{label:webPerformanceRunSiteAudit}}** for all enabled pages. Results update after successful tests; the Performance section shows the latest results.
|
||||
- key: table:web_performance_target;section:page
|
||||
summary: 'The page address and the settings you can change: label, tier and whether it is tested.'
|
||||
- key: table:web_performance_target;section:latest
|
||||
summary: The most recent successful test scores for this page and how many regressions are open.
|
||||
- key: table:web_performance_target;section:source
|
||||
summary: Where the page came from and which content record it belongs to.
|
||||
- key: table:web_performance_target;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_target;section:webPerformancePerformance
|
||||
summary: Latest mobile and desktop test results for this page, with a note when a test is waiting.
|
||||
- key: table:web_performance_target;field:siteKey
|
||||
summary: Which site the page belongs to. Filled in automatically.
|
||||
- key: table:web_performance_target;field:url
|
||||
summary: The full address that is tested. Together with the site it is unique.
|
||||
- key: table:web_performance_target;field:path
|
||||
summary: The page address without the domain, such as /contact. Filled in automatically from the URL.
|
||||
- key: table:web_performance_target;field:source
|
||||
summary: How the page was added. Pages from the sitemap show SITEMAP.
|
||||
- key: table:web_performance_target;field:contentTable
|
||||
summary: The kind of content record this page shows, such as a page or blog post, when one could be matched.
|
||||
content: |
|
||||
Your configured URL source can link a page to a content record. Empty when no content record is linked.
|
||||
- key: table:web_performance_target;field:contentId
|
||||
summary: The id of the content record this page shows. Filled in automatically with the content kind.
|
||||
- key: table:web_performance_target;field:label
|
||||
summary: A friendly name for the page, shown in lists and charts instead of the address. Optional.
|
||||
- key: table:web_performance_target;field:tier
|
||||
summary: Choose key pages for frequent tests and site scores, or standard pages for site audits.
|
||||
content: |
|
||||
Choose from {{values:web_performance_tier}}. New pages use **{{default:web_performance_target.tier}}** unless the configured URL source assigns another tier. Scheduled tests require your host to configure a scheduler.
|
||||
- key: table:web_performance_target;field:enabled
|
||||
summary: Turn off to stop testing this page.
|
||||
content: |
|
||||
New pages start enabled. Disabled pages are skipped by the daily tests, the site audit and **{{label:webPerformanceTestPageSpeed}}**.
|
||||
- key: table:web_performance_target;field:inSitemap
|
||||
summary: Whether the page was in the sitemap the last time it was synced.
|
||||
content: |
|
||||
Turned off, not deleted, when a page leaves the sitemap. Its test history stays.
|
||||
- key: table:web_performance_target;field:lastSeenInSitemapAt
|
||||
summary: The last time a sitemap sync found this page listed.
|
||||
- key: table:web_performance_target;field:latestMobilePerformance
|
||||
summary: Performance score (0 to 100) of the latest successful mobile test. Higher is better.
|
||||
- key: table:web_performance_target;field:latestDesktopPerformance
|
||||
summary: Performance score (0 to 100) of the latest successful desktop test. Higher is better.
|
||||
- key: table:web_performance_target;field:latestAccessibility
|
||||
summary: Accessibility score (0 to 100) of the latest successful test. Higher is better.
|
||||
- key: table:web_performance_target;field:latestTestedAt
|
||||
summary: When the latest successful test of this page finished.
|
||||
- key: table:web_performance_target;field:openRegressionCount
|
||||
summary: How many unresolved regressions the page has. Filled in automatically.
|
||||
content: |
|
||||
Counts suspected and confirmed regressions for this page. Review **{{label:web_performance_regression}}** for details.
|
||||
- key: table:web_performance_lab_run;slot:help
|
||||
summary: 'Every Lighthouse test: one page, one device type, from waiting in line to its final result.'
|
||||
content: |
|
||||
**How rows get created:** Queue tests with Test page speed or Run site audit, or let configured schedules queue them.
|
||||
|
||||
Each row is a single test of one page on either mobile or desktop. Runs are created when you use **{{label:webPerformanceTestPageSpeed}}**, **{{label:webPerformanceRunSiteAudit}}**, or configured schedules. With a scheduler, the worker picks up waiting runs every minute by default.
|
||||
|
||||
Scores are 0 to 100. Lab metrics are in milliseconds except **{{label:web_performance_lab_run.cls}}**, which has no unit. If Google is busy or fails, the run is tried up to 3 times in total, waiting 1 minute and then 5 minutes between tries.
|
||||
|
||||
A run that ends as **FAILED**, **RUNTIME_ERROR** or **SKIPPED_QUOTA** can be re-queued with the **{{label:webPerformanceRetryRuns}}** action, which makes a new run and keeps the old one.
|
||||
|
||||
Nothing here can be edited. The full Lighthouse report for each successful run is kept under **{{label:web_performance_lab_report}}**, and its problems are listed under **{{label:web_performance_lab_finding}}**. Lab scores vary a little from run to run, so look at trends rather than single runs.
|
||||
- key: table:web_performance_lab_run;section:run
|
||||
summary: Which page was tested, on which device type, why, and how it ended.
|
||||
- key: table:web_performance_lab_run;section:scores
|
||||
summary: The four Lighthouse category scores, from 0 to 100.
|
||||
- key: table:web_performance_lab_run;section:metrics
|
||||
summary: Measured load times in milliseconds, plus layout shift (no unit).
|
||||
- key: table:web_performance_lab_run;section:details
|
||||
summary: Queue timing, attempts, errors and the test environment.
|
||||
- key: table:web_performance_lab_run;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_lab_run;field:targetId
|
||||
summary: The tested page this run belongs to.
|
||||
- key: table:web_performance_lab_run;field:url
|
||||
summary: The address that was tested.
|
||||
- key: table:web_performance_lab_run;field:finalUrl
|
||||
summary: The address the page ended up on after any redirects.
|
||||
content: |
|
||||
Filled in from the test result. If it differs from **{{label:web_performance_lab_run.url}}**, the page redirects.
|
||||
- key: table:web_performance_lab_run;field:strategy
|
||||
summary: 'Which device the test simulated: MOBILE (phone) or DESKTOP.'
|
||||
content: |
|
||||
Mobile and desktop are always separate runs.
|
||||
- key: table:web_performance_lab_run;field:runner
|
||||
summary: The service that ran the test. Currently PSI, Google PageSpeed Insights.
|
||||
- key: table:web_performance_lab_run;field:triggerType
|
||||
summary: Why the test was queued.
|
||||
content: |
|
||||
Why the test was queued: MANUAL, SCHEDULE or SITE_AUDIT.
|
||||
|
||||
**MANUAL** means someone used **{{label:webPerformanceTestPageSpeed}}** or **{{label:webPerformanceRetryRuns}}**. **SCHEDULE** is a scheduled key-page test. **SITE_AUDIT** is the site audit of every enabled page.
|
||||
- key: table:web_performance_lab_run;field:triggerRef
|
||||
summary: A short note on what caused the run, such as the action and who used it.
|
||||
- key: table:web_performance_lab_run;field:batchId
|
||||
summary: Identifier shared by runs queued together, such as both devices of one page.
|
||||
- key: table:web_performance_lab_run;field:status
|
||||
summary: Where the run is, from queued through completion, failure or being skipped.
|
||||
content: |
|
||||
Where the run is: QUEUED, RUNNING, SUCCEEDED, RUNTIME_ERROR, FAILED or SKIPPED_QUOTA.
|
||||
|
||||
- **QUEUED**: waiting its turn (or waiting for a retry, or for a Google API key).
|
||||
- **RUNNING**: being tested now.
|
||||
- **SUCCEEDED**: finished with scores.
|
||||
- **RUNTIME_ERROR**: Lighthouse could not test the page; see **{{label:web_performance_lab_run.errorMessage}}**.
|
||||
- **FAILED**: the test failed after its attempts; see **{{label:web_performance_lab_run.errorMessage}}**.
|
||||
- **SKIPPED_QUOTA**: not run because the daily cap was reached. Use **{{label:webPerformanceRetryRuns}}**.
|
||||
- key: table:web_performance_lab_run;field:notBefore
|
||||
summary: The earliest time the run may start. Used for retry waits.
|
||||
- key: table:web_performance_lab_run;field:queuedAt
|
||||
summary: When the run was added to the queue.
|
||||
- key: table:web_performance_lab_run;field:startedAt
|
||||
summary: When the worker last picked the run up.
|
||||
- key: table:web_performance_lab_run;field:completedAt
|
||||
summary: When the run reached its final result.
|
||||
- key: table:web_performance_lab_run;field:attempts
|
||||
summary: How many times the test has been tried. A run is tried at most 3 times.
|
||||
- key: table:web_performance_lab_run;field:httpStatus
|
||||
summary: The HTTP status Google answered with. 200 means the test was returned.
|
||||
- key: table:web_performance_lab_run;field:errorMessage
|
||||
summary: Why the run failed or could not test the page. Empty on success.
|
||||
- key: table:web_performance_lab_run;field:lighthouseVersion
|
||||
summary: The Lighthouse version that scored the page.
|
||||
content: |
|
||||
Scores and audits from different major versions are not directly comparable.
|
||||
- key: table:web_performance_lab_run;field:chromeUserAgent
|
||||
summary: The browser Google used for the test.
|
||||
- key: table:web_performance_lab_run;field:benchmarkIndex
|
||||
summary: Google's measure of how fast the testing machine was at the time.
|
||||
content: |
|
||||
Lower numbers mean a slower machine, which can make scores look worse.
|
||||
- key: table:web_performance_lab_run;field:scorePerformance
|
||||
summary: Lighthouse Performance score, 0 to 100. Higher is better.
|
||||
content: |
|
||||
90 and above is good, 50 to 89 needs improvement and below 50 is poor.
|
||||
- key: table:web_performance_lab_run;field:scoreAccessibility
|
||||
summary: Lighthouse Accessibility score, 0 to 100. Higher is better.
|
||||
- key: table:web_performance_lab_run;field:scoreBestPractices
|
||||
summary: Lighthouse Best Practices score, 0 to 100. Higher is better.
|
||||
- key: table:web_performance_lab_run;field:scoreSeo
|
||||
summary: Lighthouse SEO score, 0 to 100. Higher is better.
|
||||
- key: table:web_performance_lab_run;field:fcpMs
|
||||
summary: 'First Contentful Paint in milliseconds: when anything first appears on screen.'
|
||||
- key: table:web_performance_lab_run;field:lcpMs
|
||||
summary: 'Largest Contentful Paint in milliseconds: when the biggest element on screen has loaded.'
|
||||
content: |
|
||||
Google considers 2,500 ms or less good.
|
||||
- key: table:web_performance_lab_run;field:tbtMs
|
||||
summary: 'Total Blocking Time in milliseconds: how long the page was unresponsive while loading.'
|
||||
- key: table:web_performance_lab_run;field:cls
|
||||
summary: 'Cumulative Layout Shift: how much the page jumps around while loading. No unit.'
|
||||
content: |
|
||||
Google considers 0.1 or less good.
|
||||
- key: table:web_performance_lab_run;field:speedIndexMs
|
||||
summary: 'Speed Index in milliseconds: how quickly the visible part of the page fills in.'
|
||||
- key: table:web_performance_lab_run;field:lcpElement
|
||||
summary: The largest element painted, as a page selector. Useful for finding what to speed up.
|
||||
- key: table:web_performance_lab_run;field:runWarnings
|
||||
summary: Warnings Lighthouse reported about the test itself, joined together.
|
||||
- key: table:web_performance_lab_run;field:deploymentId
|
||||
summary: The observed frontend build associated with this run, when available.
|
||||
content: |
|
||||
Filled from recorded site changes when deployment information is available.
|
||||
- key: table:web_performance_lab_run;field:cdnStage
|
||||
summary: The observed CDN stage associated with this run, when available.
|
||||
content: |
|
||||
Filled from recorded site changes when CDN stage information is available.
|
||||
- key: table:web_performance_lab_finding;slot:help
|
||||
summary: The problems and tips Lighthouse reported for each test, with how much time or data fixing them could save.
|
||||
content: |
|
||||
**How rows get created:** Successful lab tests create findings for audits that did not fully pass.
|
||||
|
||||
Each row is one audit from one run that did not fully pass (scored below 0.9). Use it to find what to fix: sort by **{{label:web_performance_lab_finding.savingsMs}}** or **{{label:web_performance_lab_finding.savingsBytes}}** to see the biggest wins.
|
||||
|
||||
Findings are grouped by **{{label:web_performance_lab_finding.category}}** and **{{label:web_performance_lab_finding.kind}}**. **{{label:web_performance_lab_finding.auditId}}** is Lighthouse's own id for the check. Lighthouse 13 replaced most performance audits with new "insight" audits, so audit ids are kept with the Lighthouse version and should not be compared across major versions.
|
||||
|
||||
Nothing here can be edited. Findings are created when a run succeeds. The **{{label:web_performance_lab_finding.topItems}}** column lists up to five example files or page elements involved.
|
||||
- key: table:web_performance_lab_finding;section:finding
|
||||
summary: What was found, its kind and category, and the estimated saving.
|
||||
- key: table:web_performance_lab_finding;section:details
|
||||
summary: The run and page it came from, the audit id, the raw score and supporting detail.
|
||||
- key: table:web_performance_lab_finding;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_lab_finding;field:runId
|
||||
summary: The lab run this finding came from.
|
||||
- key: table:web_performance_lab_finding;field:targetId
|
||||
summary: The tested page this finding applies to.
|
||||
- key: table:web_performance_lab_finding;field:strategy
|
||||
summary: 'The device type of the run: MOBILE or DESKTOP.'
|
||||
- key: table:web_performance_lab_finding;field:category
|
||||
summary: Which Lighthouse category the audit belongs to.
|
||||
content: |
|
||||
Which Lighthouse category the audit belongs to: PERFORMANCE, ACCESSIBILITY, BEST_PRACTICES or SEO.
|
||||
- key: table:web_performance_lab_finding;field:auditId
|
||||
summary: Lighthouse's own name for the check, such as a short hyphenated id.
|
||||
content: |
|
||||
Useful for searching Google's documentation. Ids change between Lighthouse major versions.
|
||||
- key: table:web_performance_lab_finding;field:lighthouseVersion
|
||||
summary: The Lighthouse version that produced the finding.
|
||||
- key: table:web_performance_lab_finding;field:kind
|
||||
summary: The type of finding returned by Lighthouse.
|
||||
content: |
|
||||
The type of finding: INSIGHT, OPPORTUNITY, DIAGNOSTIC or FAILED_AUDIT.
|
||||
|
||||
- **INSIGHT**: a performance audit from Lighthouse 13 (audit ids ending in "-insight").
|
||||
- **OPPORTUNITY**: a performance fix with an estimated saving.
|
||||
- **DIAGNOSTIC**: other performance information worth reviewing.
|
||||
- **FAILED_AUDIT**: a failed check in Accessibility, Best Practices or SEO.
|
||||
- key: table:web_performance_lab_finding;field:title
|
||||
summary: The plain-language name of the problem, as Lighthouse words it.
|
||||
- key: table:web_performance_lab_finding;field:score
|
||||
summary: The audit's own score from 0 to 1. Only audits below 0.9 are kept.
|
||||
- key: table:web_performance_lab_finding;field:displayValue
|
||||
summary: A short result Lighthouse shows with the audit, such as a size or time.
|
||||
- key: table:web_performance_lab_finding;field:savingsMs
|
||||
summary: Estimated time saved in milliseconds if this is fixed. Empty when Lighthouse gives no estimate.
|
||||
- key: table:web_performance_lab_finding;field:savingsBytes
|
||||
summary: Estimated data saved in bytes if this is fixed. Empty when Lighthouse gives no estimate.
|
||||
- key: table:web_performance_lab_finding;field:metricSavings
|
||||
summary: Estimated improvement to each metric, as raw Lighthouse data.
|
||||
- key: table:web_performance_lab_finding;field:topItems
|
||||
summary: Up to five example addresses or page elements involved, as a list.
|
||||
- key: table:web_performance_regression;slot:help
|
||||
summary: Drops in speed or breaches of a budget, kept as records to review and resolve.
|
||||
content: |
|
||||
**How rows get created:** Evaluation of lab and field results creates regressions for budget breaches and performance changes.
|
||||
|
||||
Review pages that breached a budget or changed from their baseline. Evaluation creates suspected regressions and uses further evidence to confirm or reject them; recovery can resolve them automatically.
|
||||
|
||||
Use **{{label:webPerformanceResolveRegressions}}** when you have fixed a problem, or **{{label:webPerformanceDismissRegressions}}** to close a report you choose not to act on. Both require a resolution note and management permission. You cannot edit rows directly.
|
||||
|
||||
Check **{{label:web_performance_budget}}** for thresholds and **{{label:web_performance_change_event}}** for possible causes. An empty list can also mean there is not enough test data yet.
|
||||
- key: table:web_performance_regression;section:regression
|
||||
summary: Which page and metric regressed, from what to what, and its severity and status.
|
||||
- key: table:web_performance_regression;section:details
|
||||
summary: Sources of the two values, the budget and likely cause, key dates and any note.
|
||||
- key: table:web_performance_regression;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_regression;field:targetId
|
||||
summary: The tested page that regressed.
|
||||
- key: table:web_performance_regression;field:contentTable
|
||||
summary: The kind of content record the page shows, when known.
|
||||
- key: table:web_performance_regression;field:contentId
|
||||
summary: The id of the content record the page shows, when known.
|
||||
- key: table:web_performance_regression;field:source
|
||||
summary: 'Where the measurement came from: LAB (Lighthouse), RUM (real visitors) or CRUX (Google field data).'
|
||||
- key: table:web_performance_regression;field:strategy
|
||||
summary: 'The device type: MOBILE or DESKTOP for lab results, or a form factor for field data.'
|
||||
- key: table:web_performance_regression;field:metric
|
||||
summary: Which measurement regressed.
|
||||
content: |
|
||||
Which measurement regressed, such as LCP, CLS or PERFORMANCE_SCORE.
|
||||
- key: table:web_performance_regression;field:baselineValue
|
||||
summary: The earlier, healthy value the current one is compared with.
|
||||
- key: table:web_performance_regression;field:baselineRef
|
||||
summary: Where the baseline value came from, such as a run or date range.
|
||||
- key: table:web_performance_regression;field:currentValue
|
||||
summary: The latest measured value.
|
||||
- key: table:web_performance_regression;field:currentRef
|
||||
summary: Where the current value came from, such as a run or date range.
|
||||
- key: table:web_performance_regression;field:delta
|
||||
summary: 'Current minus baseline. Units follow the metric: milliseconds, score points or no unit for CLS.'
|
||||
- key: table:web_performance_regression;field:budgetId
|
||||
summary: The budget that was breached, if the regression came from one.
|
||||
- key: table:web_performance_regression;field:severity
|
||||
summary: 'How serious it is: WARNING or CRITICAL, taken from the budget.'
|
||||
- key: table:web_performance_regression;field:status
|
||||
summary: Where the regression stands, from suspected through confirmation, resolution or dismissal.
|
||||
content: |
|
||||
Where it stands: SUSPECTED, CONFIRMED, NOT_CONFIRMED, RESOLVED or DISMISSED.
|
||||
|
||||
- **SUSPECTED**: spotted, waiting for re-tests.
|
||||
- **CONFIRMED**: re-tests agreed it is real.
|
||||
- **NOT_CONFIRMED**: re-tests did not reproduce it; the record stays visible.
|
||||
- **RESOLVED**: the value recovered or was fixed.
|
||||
- **DISMISSED**: someone decided to ignore it.
|
||||
- key: table:web_performance_regression;field:causeChangeEventId
|
||||
summary: The site change that most likely caused it, such as a deploy.
|
||||
- key: table:web_performance_regression;field:detectedAt
|
||||
summary: When the regression was first noticed.
|
||||
- key: table:web_performance_regression;field:confirmedAt
|
||||
summary: When a re-test confirmed it.
|
||||
- key: table:web_performance_regression;field:resolvedAt
|
||||
summary: When it was resolved or dismissed.
|
||||
- key: table:web_performance_regression;field:resolutionNote
|
||||
summary: A note on how it was resolved or why it was dismissed.
|
||||
- key: table:web_performance_budget;slot:help
|
||||
summary: The thresholds that define when a speed measurement is good enough, by scope, metric and device type.
|
||||
content: |
|
||||
**How rows get created:** The shipped database changelog adds reference budgets; you can add your own with New.
|
||||
|
||||
Set the limits you want your pages to meet. Choose a scope, measurement source, device type, metric, comparison, threshold and severity. Times use milliseconds, scores use points and layout shift has no unit.
|
||||
|
||||
The shipped database changelog supplies reference budgets. You can edit them, add your own or turn them off with **{{label:web_performance_budget.enabled}}**. Changes are audited.
|
||||
|
||||
Enabled lab and URL Google field-data budgets are evaluated as results arrive. Breaches appear in **{{label:web_performance_regression}}**. Real-user budgets are not evaluated until a real-user source is supported.
|
||||
- key: table:web_performance_budget;section:budget
|
||||
summary: 'The rule itself: the metric, comparison, threshold, severity and whether it is on.'
|
||||
- key: table:web_performance_budget;section:scope
|
||||
summary: 'What the budget applies to: scope, scope value, source and device type.'
|
||||
- key: table:web_performance_budget;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_budget;field:name
|
||||
summary: A short description of the rule, such as "LCP good (lab, mobile)".
|
||||
- key: table:web_performance_budget;field:scope
|
||||
summary: What the budget covers.
|
||||
content: |
|
||||
Choose the whole site, a tier, a page, a content table or a path prefix. Scope and scope value work together: Site needs no value; Tier takes the stored tier value, such as KEY.
|
||||
- key: table:web_performance_budget;field:scopeValue
|
||||
summary: The specific tier, page, content kind or path the scope points at. Leave empty for the whole site.
|
||||
- key: table:web_performance_budget;field:source
|
||||
summary: 'Which data the budget checks: LAB (Lighthouse), RUM (real visitors) or CRUX (Google field data).'
|
||||
- key: table:web_performance_budget;field:strategy
|
||||
summary: Choose a lab device or field-data form factor; leave empty to apply to all devices.
|
||||
- key: table:web_performance_budget;field:metric
|
||||
summary: The measurement to check against the budget.
|
||||
content: |
|
||||
The measurement to check, such as LCP, CLS, FCP, INP, TTFB or PERFORMANCE_SCORE.
|
||||
|
||||
Lab score metrics are PERFORMANCE_SCORE, ACCESSIBILITY_SCORE, BEST_PRACTICES_SCORE and SEO_SCORE. Times are in milliseconds; CLS has no unit; scores are 0 to 100.
|
||||
- key: table:web_performance_budget;field:comparator
|
||||
summary: Whether the measurement must be at or below the threshold, or at or above it.
|
||||
content: |
|
||||
How to compare: <= means the value must be at or below the threshold, >= at or above.
|
||||
- key: table:web_performance_budget;field:threshold
|
||||
summary: The limit the metric is compared with. Use the metric's own unit, such as milliseconds or score points.
|
||||
- key: table:web_performance_budget;field:severity
|
||||
summary: 'How serious a breach is: WARNING or CRITICAL.'
|
||||
- key: table:web_performance_budget;field:enabled
|
||||
summary: Turn off to ignore this budget without deleting it.
|
||||
- key: table:web_performance_budget;field:seeded
|
||||
summary: On for the reference budgets that shipped with the app. Filled in automatically; you can still edit those
|
||||
rows.
|
||||
- key: table:web_performance_change_event;slot:help
|
||||
summary: Things that may have changed speed, such as a deploy, a CDN change or a note, used as markers on trends.
|
||||
content: |
|
||||
**How rows get created:** Configured probes and change sources record observed changes; lab evaluation also records Lighthouse version changes.
|
||||
|
||||
Review deployments and other observed site changes to understand changes in performance. Configured probes and change sources create these records; repeated observations update the existing event. Lighthouse major-version changes are also recorded.
|
||||
|
||||
You cannot edit these rows directly. Use **{{label:webPerformanceCompareChange}}** to compare measurements around a selected change when comparisons are enabled. See **{{label:web_performance_comparison}}** for the results.
|
||||
- key: table:web_performance_change_event;section:change
|
||||
summary: What changed, in which component, from what to what, and when it was seen.
|
||||
- key: table:web_performance_change_event;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_change_event;field:kind
|
||||
summary: The type of change that may affect performance.
|
||||
content: |
|
||||
The type of change: FRONTEND_DEPLOY, BACKEND_DEPLOY, CDN_STAGE, LIGHTHOUSE_VERSION or MANUAL.
|
||||
- key: table:web_performance_change_event;field:component
|
||||
summary: The part of the site that changed, such as FRONTEND.
|
||||
- key: table:web_performance_change_event;field:fromValue
|
||||
summary: The value before the change, such as the old build id.
|
||||
- key: table:web_performance_change_event;field:toValue
|
||||
summary: The value after the change, such as the new build id.
|
||||
- key: table:web_performance_change_event;field:observedAt
|
||||
summary: When the change was first noticed.
|
||||
- key: table:web_performance_change_event;field:source
|
||||
summary: What recorded the change.
|
||||
content: |
|
||||
What recorded the change: PROBE (a build check), WEBHOOK, QBIT_CDN (the CDN setup) or USER (a person).
|
||||
- key: table:web_performance_change_event;field:note
|
||||
summary: Free text describing the change.
|
||||
- key: table:web_performance_comparison;slot:help
|
||||
summary: Measurements compared before and after a site change, per metric, device type and segment.
|
||||
content: |
|
||||
**How rows get created:** Run Compare before and after on a site change when enough samples are available.
|
||||
|
||||
Use **{{label:webPerformanceCompareChange}}** on a site change to create before-and-after comparisons. Lab comparisons need at least three successful samples on each side using the same Lighthouse major version.
|
||||
|
||||
Review the metric, device type, sample counts and time windows alongside the before and after values. **{{label:web_performance_comparison.deltaPct}}** shows the percentage change. You cannot edit these results directly; an empty list can mean no comparison has run or there are too few samples.
|
||||
- key: table:web_performance_comparison;section:comparison
|
||||
summary: The change, the metric and segment, the before and after values and how far they moved.
|
||||
- key: table:web_performance_comparison;section:details
|
||||
summary: Scope, data source, device type, sample counts and the time windows compared.
|
||||
- key: table:web_performance_comparison;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_comparison;field:changeEventId
|
||||
summary: The site change this comparison is around.
|
||||
- key: table:web_performance_comparison;field:scope
|
||||
summary: 'What was compared: SITE, TARGET (one page) or KEY (the key page tier).'
|
||||
- key: table:web_performance_comparison;field:source
|
||||
summary: The measurement source used for the comparison. Lab comparisons are currently implemented.
|
||||
- key: table:web_performance_comparison;field:strategy
|
||||
summary: The device type or form factor compared, such as MOBILE or DESKTOP.
|
||||
- key: table:web_performance_comparison;field:metric
|
||||
summary: The measurement compared.
|
||||
content: |
|
||||
The measurement compared, such as LCP or PERFORMANCE_SCORE.
|
||||
- key: table:web_performance_comparison;field:segment
|
||||
summary: The slice of visitors or pages compared, such as a country. Empty for everything.
|
||||
- key: table:web_performance_comparison;field:beforeValue
|
||||
summary: The value before the change, as a median or 75th percentile.
|
||||
- key: table:web_performance_comparison;field:beforeN
|
||||
summary: How many measurements went into the before value. More samples mean a more reliable number.
|
||||
- key: table:web_performance_comparison;field:beforeWindow
|
||||
summary: The time span of the before measurements.
|
||||
- key: table:web_performance_comparison;field:afterValue
|
||||
summary: The value after the change, as a median or 75th percentile.
|
||||
- key: table:web_performance_comparison;field:afterN
|
||||
summary: How many measurements went into the after value. More samples mean a more reliable number.
|
||||
- key: table:web_performance_comparison;field:afterWindow
|
||||
summary: The time span of the after measurements.
|
||||
- key: table:web_performance_comparison;field:delta
|
||||
summary: After minus before, in the metric's own unit.
|
||||
- key: table:web_performance_comparison;field:deltaPct
|
||||
summary: The same difference as a percentage of the before value.
|
||||
- key: table:web_performance_field_snapshot;slot:help
|
||||
summary: What real Chrome users experienced on the site over the last 28 days, as reported by Google (CrUX).
|
||||
content: |
|
||||
**How rows get created:** Refresh Google field data, manually or through a configured schedule, to store snapshots.
|
||||
|
||||
Google collects speed data from Chrome users and publishes the 75th percentile (p75) of each Core Web Vital over a rolling 28-day period. Each row is one check for the whole site (origin) or one page (URL), and one form factor: ALL, PHONE or DESKTOP.
|
||||
|
||||
If Google has no data (common for low-traffic pages), the row is recorded as **NO_DATA** instead of left out, and re-checking only moves **{{label:web_performance_field_snapshot.lastCheckedAt}}**. Rows with data are stored once per 28-day period.
|
||||
|
||||
With a scheduler configured, data refreshes daily by default, or on demand with **{{label:webPerformanceRefreshFieldData}}** (admin permission). It needs a Google API key. Nothing here can be edited. See also **{{label:web_performance_lab_run}}**, which are lab tests, not real users.
|
||||
- key: table:web_performance_field_snapshot;section:snapshot
|
||||
summary: 'What was checked: the site or page, the form factor, the result and when it was last checked.'
|
||||
- key: table:web_performance_field_snapshot;section:p75
|
||||
summary: The 75th percentile of each Core Web Vital over the 28-day period, plus the period dates.
|
||||
- key: table:web_performance_field_snapshot;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_field_snapshot;field:source
|
||||
summary: Which Google service provided the data.
|
||||
content: |
|
||||
Which Google service provided the data. Currently CRUX_API.
|
||||
- key: table:web_performance_field_snapshot;field:scope
|
||||
summary: Whether the row covers the whole site (ORIGIN) or a single page (URL).
|
||||
- key: table:web_performance_field_snapshot;field:scopeKey
|
||||
summary: The site origin or the page address that was checked.
|
||||
- key: table:web_performance_field_snapshot;field:formFactor
|
||||
summary: 'The devices covered: ALL, PHONE or DESKTOP.'
|
||||
- key: table:web_performance_field_snapshot;field:status
|
||||
summary: Whether Google returned data for this snapshot.
|
||||
content: |
|
||||
OK when Google returned data, NO_DATA when it had none.
|
||||
- key: table:web_performance_field_snapshot;field:periodStart
|
||||
summary: First day of Google's 28-day collection period. Empty when there is no data.
|
||||
content: |
|
||||
First day of Google's 28-day collection period. Empty for NO_DATA.
|
||||
- key: table:web_performance_field_snapshot;field:periodEnd
|
||||
summary: Last day of Google's 28-day collection period. Empty when there is no data.
|
||||
content: |
|
||||
Last day of Google's 28-day collection period. Empty for NO_DATA.
|
||||
- key: table:web_performance_field_snapshot;field:p75LcpMs
|
||||
summary: Largest Contentful Paint at the 75th percentile, in milliseconds. 2,500 or less is good.
|
||||
- key: table:web_performance_field_snapshot;field:p75InpMs
|
||||
summary: Interaction to Next Paint at the 75th percentile, in milliseconds. 200 or less is good.
|
||||
- key: table:web_performance_field_snapshot;field:p75Cls
|
||||
summary: Cumulative Layout Shift at the 75th percentile, with no unit. 0.1 or less is good.
|
||||
- key: table:web_performance_field_snapshot;field:p75FcpMs
|
||||
summary: First Contentful Paint at the 75th percentile, in milliseconds. 1,800 or less is good.
|
||||
- key: table:web_performance_field_snapshot;field:p75TtfbMs
|
||||
summary: Time to First Byte at the 75th percentile, in milliseconds. 800 or less is good.
|
||||
- key: table:web_performance_field_snapshot;field:distributions
|
||||
summary: How users split between good, needs improvement and poor for each metric, as raw data.
|
||||
- key: table:web_performance_field_snapshot;field:lastCheckedAt
|
||||
summary: The last time Google was asked about this page or site, even if nothing changed.
|
||||
- key: table:web_performance_lab_report;slot:help
|
||||
summary: The complete Lighthouse report of each successful test, kept compressed for later download.
|
||||
content: |
|
||||
**How rows get created:** Successful lab tests store their complete compressed responses here.
|
||||
|
||||
Open a report when you need the complete Lighthouse response behind a run. Each successful run has at most one compressed report. Download and decompress its content to inspect the JSON.
|
||||
|
||||
**{{label:web_performance_lab_report.sha256}}** lets you verify the uncompressed copy. Reports are kept indefinitely by default; your host can configure retention to delete older reports while keeping run scores and findings. You cannot edit reports directly. See **{{label:web_performance_lab_run}}** and **{{label:web_performance_lab_finding}}** for readable results.
|
||||
- key: table:web_performance_lab_report;section:report
|
||||
summary: Which run the report belongs to, its size and checksum, and the downloadable content.
|
||||
- key: table:web_performance_lab_report;section:dates
|
||||
summary: When the record was created and last changed.
|
||||
- key: table:web_performance_lab_report;field:runId
|
||||
summary: The lab run this report belongs to. Each run has at most one.
|
||||
- key: table:web_performance_lab_report;field:content
|
||||
summary: The full report as compressed JSON. Download it to read it in a Lighthouse viewer or a text editor after unzipping.
|
||||
- key: table:web_performance_lab_report;field:bytes
|
||||
summary: The size of the report before compression, in bytes.
|
||||
- key: table:web_performance_lab_report;field:sha256
|
||||
summary: A checksum of the uncompressed report, to verify a copy is complete.
|
||||
- key: process:webPerformanceTestPageSpeed;slot:help
|
||||
summary: Queues Google PageSpeed (Lighthouse) tests for the selected pages, for each configured device strategy.
|
||||
content: |
|
||||
Select tested pages, then run this action. A review screen shows how many runs will be queued before anything is queued.
|
||||
|
||||
- Each configured device strategy creates a separate run; mobile and desktop are enabled by default.
|
||||
- With a scheduler configured, tests run in the background about once a minute by default, and results appear under **{{label:web_performance_lab_run}}** and the Performance dashboard.
|
||||
- If a page already has a waiting run, it is merged rather than queued twice.
|
||||
- Your host configures a daily run cap. Runs over the cap are recorded as skipped; use **{{label:webPerformanceRetryRuns}}** the next day.
|
||||
- Content records with no tested page (a draft, or not in the sitemap) and disabled pages are skipped.
|
||||
- Without a Google API key the runs wait in the queue until one is configured.
|
||||
|
||||
You need the page speed testing permission.
|
||||
- key: process:webPerformanceRunSiteAudit;slot:help
|
||||
summary: Queues a speed test for every enabled tested page, for each configured device strategy.
|
||||
content: |
|
||||
Use it to get a fresh picture of the whole site, for example after a release. The review screen lists the enabled pages that will be tested; disabled pages are left out. Run it from **{{label:web_performance_target}}**.
|
||||
|
||||
Tests then run in the background. A site audit can use much of your configured daily run cap, so avoid running it repeatedly. Runs that do not fit are recorded as skipped and can be re-queued with **{{label:webPerformanceRetryRuns}}**.
|
||||
|
||||
You need the page speed testing permission. See also **{{label:webPerformanceScheduleSiteAudit}}**, which queues a weekly audit by default when a scheduler is configured.
|
||||
- key: process:webPerformanceRetryRuns;slot:help
|
||||
summary: Queues a new test for lab runs that failed, hit an error or were skipped for the daily cap.
|
||||
content: |
|
||||
Select runs in **{{label:web_performance_lab_run}}** and run this action. Only runs that failed, ended with a runtime error or were skipped because of the daily cap are retried; successful and waiting runs are listed as not retried. Each retry is a new run for the same page and device type.
|
||||
|
||||
Transient failures are tried up to three times in total, with one-minute and five-minute waits; use this action for runs that still ended failed or skipped.
|
||||
|
||||
You need the page speed testing permission.
|
||||
- key: process:webPerformanceSyncPages;slot:help
|
||||
summary: Updates tested pages from your configured URL source.
|
||||
content: |
|
||||
Review the pages returned by your configured URL source before applying the changes. New pages are added and enabled; existing pages are kept. Pages that leave the source are marked as absent but retain their history.
|
||||
|
||||
Use this after publishing pages when you want to test them sooner. Automatic syncing requires a configured scheduler. You need web performance administrator permission.
|
||||
- key: process:webPerformanceRefreshFieldData;slot:help
|
||||
summary: Asks Google for its real-user speed data about the site and its tested pages.
|
||||
content: |
|
||||
This queries Google's Chrome User Experience Report for the whole site and for each enabled page, once per device type. Results with no data are stored as no data, because Google only publishes figures for sites and pages with enough Chrome traffic. The review screen shows how many queries will be made without calling Google.
|
||||
|
||||
It also runs automatically when your host configures a scheduler. It needs a Google API key; without one it reports that field data cannot be checked.
|
||||
|
||||
You need the web performance administrator permission. Results show in **{{label:web_performance_field_snapshot}}**.
|
||||
- key: process:webPerformanceRunQueuedTests;slot:help
|
||||
summary: Runs due page-speed tests in the background.
|
||||
content: |
|
||||
With a scheduler configured, the worker checks the queue every minute by default. Transient failures are tried up to three times in total, with one-minute and five-minute waits. You normally do not need to run this yourself.
|
||||
- key: process:webPerformanceScheduleKeyTests;slot:help
|
||||
summary: Queues tests for enabled key pages on the configured schedule.
|
||||
content: |
|
||||
Set a page to the key-page tier to include it. With a scheduler configured, the default is daily in your host's configured time zone. Your host can change the schedule. Runs wait for the queue worker.
|
||||
- key: process:webPerformanceScheduleSiteAudit;slot:help
|
||||
summary: Queues tests for all enabled pages on the configured site-audit schedule.
|
||||
content: |
|
||||
With a scheduler configured, the default site audit runs weekly in your host's configured time zone. Your host can change the schedule. Runs count toward the daily limit; excess runs are recorded as skipped.
|
||||
- key: process:webPerformanceScheduledSync;slot:help
|
||||
summary: Updates tested pages from the configured URL source on a schedule.
|
||||
content: |
|
||||
With a scheduler configured, this runs daily by default. Like **{{label:webPerformanceSyncPages}}**, it adds pages and marks pages that left the source without deleting their history. Your host can change the schedule.
|
||||
- key: process:webPerformanceScheduledFieldData;slot:help
|
||||
summary: Refreshes Google field data on the configured schedule.
|
||||
content: |
|
||||
With a scheduler configured, this runs daily by default. Like **{{label:webPerformanceRefreshFieldData}}**, it checks your origin and enabled pages. It skips the refresh when no field-data client is configured.
|
||||
- key: widget:webPerformanceSiteScores;slot:label
|
||||
summary: Median Lighthouse scores (Performance, Accessibility, Best Practices, SEO) for the key pages, mobile and desktop.
|
||||
- key: widget:webPerformanceTrend;slot:label
|
||||
summary: Weekly median Performance score for mobile and desktop, from the first week with a successful test.
|
||||
- key: widget:webPerformanceSlowestPages;slot:label
|
||||
summary: The ten tested pages with the lowest latest mobile Performance score, with their lab LCP and top finding.
|
||||
- key: widget:webPerformanceTopOpportunities;slot:label
|
||||
summary: The audit findings affecting the most pages in each page's latest test, with the estimated time savings.
|
||||
- key: widget:webPerformanceFieldDataStatus;slot:label
|
||||
summary: Google's real-user LCP, INP, CLS and TTFB for the site over the last 28 days, or why none is available.
|
||||
- key: widget:webPerformanceRecentRuns;slot:label
|
||||
summary: The five most recent page speed test runs with status and scores, and how many runs are waiting.
|
||||
- key: widget:webPerformanceRecordPanel;slot:label
|
||||
summary: This page's latest speed scores for mobile and desktop, key lab metrics, what to fix and a mobile score trend.
|
||||
- key: table:web_performance_lab_run;field:ttfbMs
|
||||
summary: Time to first byte measured by the lab run, in milliseconds.
|
||||
- key: table:web_performance_lab_run;field:evaluatedAt
|
||||
summary: When this run was processed by the regression evaluator; blank until evaluated.
|
||||
- key: table:web_performance_lab_run;field:changeEventId
|
||||
summary: The site change associated with this lab run.
|
||||
content: |
|
||||
See **{{label:web_performance_change_event}}** for the deployment or other change being measured.
|
||||
- key: table:web_performance_regression;field:firstRunId
|
||||
summary: The lab run that first raised this regression.
|
||||
- key: table:web_performance_regression;field:checkpointRunId
|
||||
summary: The lab run used as the latest confirmation checkpoint.
|
||||
content: |
|
||||
Initially the first run; updated when the regression is confirmed. See **{{label:web_performance_lab_run}}** for its measurements.
|
||||
- key: table:web_performance_regression;field:lighthouseMajor
|
||||
summary: The Lighthouse major version used to keep regression comparisons compatible.
|
||||
- key: table:web_performance_regression;field:failedAudits
|
||||
summary: Comma-separated audit identifiers that newly failed when this regression was raised.
|
||||
- key: table:web_performance_change_event;field:lastSeenAt
|
||||
summary: The latest time the change source reported this same event.
|
||||
- key: table:web_performance_change_event;field:eventKey
|
||||
summary: Generated key that prevents repeated observations from creating duplicate change events.
|
||||
- key: table:web_performance_change_event;field:siteKey
|
||||
summary: The configured site this change belongs to.
|
||||
- key: table:web_performance_comparison;field:targetId
|
||||
summary: The page whose measurements are compared before and after the change.
|
||||
- key: table:web_performance_field_snapshot;field:evaluatedAt
|
||||
summary: When this field-data snapshot was processed by the regression evaluator.
|
||||
- key: process:webPerformanceDismissRegressions;slot:help
|
||||
summary: Dismiss selected open regressions with a required resolution note.
|
||||
content: |
|
||||
Review the preview before continuing. Closed regressions are skipped. Use **{{label:webPerformanceResolveRegressions}}** when the problem has been fixed.
|
||||
- key: process:webPerformanceResolveRegressions;slot:help
|
||||
summary: Mark selected open regressions resolved with a required resolution note.
|
||||
content: |
|
||||
Review the preview before continuing. Closed regressions are skipped; this records your decision without running another page-speed test.
|
||||
- key: process:webPerformanceCompareChange;slot:help
|
||||
summary: Compare page performance before and after selected site changes.
|
||||
content: |
|
||||
Creates **{{label:web_performance_comparison}}** records. Lab comparisons need at least three successful samples on each side using the same Lighthouse major version.
|
||||
- key: process:webPerformanceProbeDeploys;slot:help
|
||||
summary: Check configured sources for deployments and other site changes.
|
||||
content: |
|
||||
The scheduled check records observations in **{{label:web_performance_change_event}}**. Repeated observations update the existing event instead of creating duplicates.
|
||||
- key: widget:webPerformanceSetup;slot:label
|
||||
summary: Admin only. Check your tested pages, lab runner, budgets and field-data setup, with the next step for each.
|
||||
- key: widget:webPerformanceNotConfigured;slot:label
|
||||
summary: Says when page speed testing is not set up and who can fix it.
|
||||
- key: possibleValueSource:web_performance_target;slot:help
|
||||
summary: Choose a tested page.
|
||||
- key: possibleValueSource:web_performance_lab_run;slot:help
|
||||
summary: Choose a lab test run.
|
||||
- key: possibleValueSource:web_performance_budget;slot:help
|
||||
summary: Choose a performance budget.
|
||||
- key: possibleValueSource:web_performance_change_event;slot:help
|
||||
summary: Choose an observed site change.
|
||||
- key: possibleValueSource:web_performance_regression;slot:help
|
||||
summary: Choose a performance regression.
|
||||
- key: possibleValueSource:web_performance_tier;slot:help
|
||||
summary: Choose how frequently a page should be tested.
|
||||
- key: possibleValueSource:web_performance_target_source;slot:help
|
||||
summary: How a tested page was discovered.
|
||||
- key: possibleValueSource:web_performance_strategy;slot:help
|
||||
summary: Choose a device strategy for lab testing.
|
||||
- key: possibleValueSource:web_performance_strategy_or_form_factor;slot:help
|
||||
summary: Choose a lab device strategy or field-data form factor.
|
||||
- key: possibleValueSource:web_performance_field_source;slot:help
|
||||
summary: The Google service that supplied field data.
|
||||
- key: possibleValueSource:web_performance_field_scope;slot:help
|
||||
summary: Choose field data for one page or an entire origin.
|
||||
- key: possibleValueSource:web_performance_budget_scope;slot:help
|
||||
summary: Choose which pages a budget applies to.
|
||||
- key: possibleValueSource:web_performance_measurement_source;slot:help
|
||||
summary: Choose the source of performance measurements.
|
||||
- key: possibleValueSource:web_performance_comparison_source;slot:help
|
||||
summary: Choose the source of before-and-after measurements.
|
||||
- key: possibleValueSource:web_performance_comparison_scope;slot:help
|
||||
summary: Choose which pages to compare.
|
||||
- key: possibleValueSource:web_performance_change_source;slot:help
|
||||
summary: How a site change was reported.
|
||||
- key: possibleValueSource:web_performance_metric;slot:help
|
||||
summary: Choose the performance measurement to evaluate.
|
||||
- key: possibleValueSource:web_performance_comparator;slot:help
|
||||
summary: Choose whether a value must be at most or at least the threshold.
|
||||
- key: possibleValueSource:web_performance_severity;slot:help
|
||||
summary: Choose how serious a budget breach is.
|
||||
@@ -0,0 +1,58 @@
|
||||
bundle: web-performance
|
||||
version: 1
|
||||
layer: QBIT
|
||||
# Help for the Performance app and every web_performance_* table, in the QQQ
|
||||
# help-bundle format (qqq#1198). Each table entry says what the table is for and
|
||||
# how its rows get created, so an empty table explains itself. WebPerfHelp
|
||||
# applies it until the core HelpBundles registry ships in a QQQ release.
|
||||
# Summaries stay under 120 characters (WebPerfTablesGuardrailTest).
|
||||
entries:
|
||||
- key: app:webPerformance;slot:help
|
||||
summary: "Page speed for your site: Lighthouse lab tests, Google field data, budgets and regressions."
|
||||
content: |
|
||||
The Setup card on the home lists what is configured and the next step: sync the pages, give the qbit a Google API key, and review the budgets. A new installation starts with reference budgets (Google's good thresholds) already in place.
|
||||
|
||||
- key: table:web_performance_target;slot:help
|
||||
summary: "The pages that get tested: one row per URL, with its tier, latest scores and open regressions."
|
||||
content: |
|
||||
**How rows get created:** run Sync pages from sitemap (also scheduled), which adds every page the host's URL source returns and links it to its content record. You can also add a page by hand with New. Mark a page KEY to put it in the site scores and the frequent tests. Run site audit or Test page speed queues lab tests.
|
||||
|
||||
- key: table:web_performance_lab_run;slot:help
|
||||
summary: "Every Lighthouse test of a page: queued, running, finished or failed, with its scores and timings."
|
||||
content: |
|
||||
**How rows get created:** by the qbit. Test page speed, Run site audit and the schedules queue runs here, and the worker runs them through the lab runner (PageSpeed Insights when a Google API key is set). Nothing is added by hand. Without a key, runs wait in the queue. Retry re-queues a failed run.
|
||||
|
||||
- key: table:web_performance_lab_finding;slot:help
|
||||
summary: "What Lighthouse says to fix on each tested page: the audits that failed and how much they would save."
|
||||
content: |
|
||||
**How rows get created:** by the qbit, from each finished lab run. One row per failed or opportunity audit. It stays empty until a lab test succeeds.
|
||||
|
||||
- key: table:web_performance_lab_report;slot:help
|
||||
summary: "The complete Lighthouse response of each run, compressed, for download and later comparison."
|
||||
content: |
|
||||
**How rows get created:** by the qbit, one per successful lab run. Nothing to set up. Reports are kept forever unless the host sets reportRetentionDays, which deletes older reports (the run and its scores stay).
|
||||
|
||||
- key: table:web_performance_field_snapshot;slot:help
|
||||
summary: "What Google's field data (CrUX) says real Chrome users experience: p75 Core Web Vitals per URL or origin."
|
||||
content: |
|
||||
**How rows get created:** by the qbit. Refresh Google field data (and the daily schedule) asks Google for the site and each tested page. It needs the Google API key. A row says NO_DATA when Google has too little traffic to publish figures for that page.
|
||||
|
||||
- key: table:web_performance_budget;slot:help
|
||||
summary: "Limits a metric must stay within, per site, tier, page, content table or path; a breach raises a regression."
|
||||
content: |
|
||||
**How rows get created:** the qbit seeds reference budgets (Google's good thresholds for each Core Web Vital, and Lighthouse category scores for KEY pages), so a new installation already flags slow pages. Edit them, turn them off, or add your own with New. Lab and Google field data (CRUX) budgets are evaluated; budgets with source RUM (real users) are not evaluated yet, because no real-user source is wired in.
|
||||
|
||||
- key: table:web_performance_regression;slot:help
|
||||
summary: "A page that got slower or broke a budget: suspected, confirmed, then resolved or dismissed."
|
||||
content: |
|
||||
**How rows get created:** by the qbit. When a test breaks a budget or drifts from its baseline, it opens a suspected regression and confirms it after more runs. Mark resolved and Dismiss close one by hand. Empty means nothing is wrong.
|
||||
|
||||
- key: table:web_performance_change_event;slot:help
|
||||
summary: "Site changes (a deploy, a Lighthouse version, a CDN stage change) that trigger re-tests."
|
||||
content: |
|
||||
**How rows get created:** by the qbit. Check site changes (scheduled when deploy probes or change sources are configured) records a change, and the qbit notes when Lighthouse itself changes version. It stays empty until the host configures a deploy probe or change event source.
|
||||
|
||||
- key: table:web_performance_comparison;slot:help
|
||||
summary: "Before and after results around a site change, per page and metric."
|
||||
content: |
|
||||
**How rows get created:** run Compare before and after on a change event (it needs the CDN_COMPARE feature and a change event). Nothing to add by hand.
|
||||
@@ -1 +0,0 @@
|
||||
version=${project.version}
|
||||
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.webperf;
|
||||
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import liquibase.Contexts;
|
||||
import liquibase.LabelExpression;
|
||||
import liquibase.Liquibase;
|
||||
import liquibase.database.Database;
|
||||
import liquibase.database.DatabaseFactory;
|
||||
import liquibase.database.jvm.JdbcConnection;
|
||||
import liquibase.resource.ClassLoaderResourceAccessor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Referential integrity on PostgreSQL (the production database), with the
|
||||
** changelog run by Liquibase as a host runs it:
|
||||
**
|
||||
** - every table possible-value field that names one of the QBit's tables,
|
||||
** and every join, has a database foreign key (or an exemption with a
|
||||
** reason), so a new reference cannot be added without its constraint;
|
||||
** - rows whose parent is missing when the constraints are added are recorded
|
||||
** in qqq_fk_quarantine and repaired (nulled, or deleted with a full copy),
|
||||
** so the upgrade never fails on existing data;
|
||||
** - the foreign key changesets are no-ops on a database that already has the
|
||||
** constraints (a host that added them itself).
|
||||
**
|
||||
** Skipped when Docker is not available.
|
||||
*******************************************************************************/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
class WebPerfForeignKeyTest
|
||||
{
|
||||
private static final String PREFIX = "web_performance_";
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// references that are deliberately not foreign keys: "child.column" -> //
|
||||
// reason. None today (content_table + content_id is polymorphic, and has //
|
||||
// no possible-value source). //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, String> EXEMPT = Map.of();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// the changesets before the foreign keys (1-11) //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
private static final int CHANGESETS_BEFORE_FOREIGN_KEYS = 11;
|
||||
|
||||
@Container
|
||||
private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17-alpine");
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Orphans are quarantined, every reference gets its constraint, and a
|
||||
** re-run of the foreign key changesets changes nothing.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testEveryReferenceHasAForeignKey() throws Exception
|
||||
{
|
||||
try(Connection connection = DriverManager.getConnection(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword());
|
||||
Statement statement = connection.createStatement())
|
||||
{
|
||||
statement.execute("DROP SCHEMA public CASCADE");
|
||||
statement.execute("CREATE SCHEMA public");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// an existing install with rows whose parents are gone //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
migrate(connection, CHANGESETS_BEFORE_FOREIGN_KEYS);
|
||||
statement.execute("INSERT INTO web_performance_target (id, url) VALUES (1, 'https://www.example.com/')");
|
||||
statement.execute("INSERT INTO web_performance_lab_run (id, target_id) VALUES (1, 1), (2, 404)");
|
||||
statement.execute("INSERT INTO web_performance_lab_finding (id, run_id, target_id) VALUES (1, 1, 1), (2, 404, 1)");
|
||||
statement.execute("INSERT INTO web_performance_lab_report (id, run_id) VALUES (1, 404)");
|
||||
statement.execute("INSERT INTO web_performance_regression (id, target_id, budget_id, first_run_id) VALUES (1, 1, 404, 404)");
|
||||
|
||||
migrate(connection, null);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// every reference the meta-data declares has its constraint //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
Map<String, String> foreignKeys = foreignKeys(statement);
|
||||
List<String> missing = new ArrayList<>();
|
||||
for(String reference : references())
|
||||
{
|
||||
if(!foreignKeys.containsKey(reference) && !EXEMPT.containsKey(reference))
|
||||
{
|
||||
missing.add(reference);
|
||||
}
|
||||
}
|
||||
assertThat(missing).as("references without a foreign key (add one to the changelog, or an exemption with a reason)").isEmpty();
|
||||
assertThat(foreignKeys).containsEntry("web_performance_lab_finding.run_id", "web_performance_lab_run CASCADE")
|
||||
.containsEntry("web_performance_lab_run.target_id", "web_performance_target RESTRICT")
|
||||
.containsEntry("web_performance_regression.cause_change_event_id", "web_performance_change_event SET NULL");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// the orphans were recorded, then nulled or deleted //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
assertThat(rows(statement, "SELECT table_name || '.' || column_name || ' ' || row_id || '->' || missing_parent_id || ' ' || action || ' ' || (row_data IS NOT NULL) FROM qqq_fk_quarantine"))
|
||||
.containsExactlyInAnyOrder(
|
||||
"web_performance_lab_run.target_id 2->404 NULLED false",
|
||||
"web_performance_lab_finding.run_id 2->404 DELETED true",
|
||||
"web_performance_lab_report.run_id 1->404 DELETED true",
|
||||
"web_performance_regression.budget_id 1->404 NULLED false",
|
||||
"web_performance_regression.first_run_id 1->404 NULLED false");
|
||||
assertThat(rows(statement, "SELECT id || ' ' || COALESCE(CAST(target_id AS TEXT), '-') FROM web_performance_lab_run")).containsExactlyInAnyOrder("1 1", "2 -");
|
||||
assertThat(rows(statement, "SELECT CAST(id AS TEXT) FROM web_performance_lab_finding")).containsExactly("1");
|
||||
assertThat(rows(statement, "SELECT CAST(id AS TEXT) FROM web_performance_lab_report")).isEmpty();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// a host that already has the constraints: the changesets are marked //
|
||||
// ran and change nothing //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
statement.execute("DELETE FROM databasechangelog WHERE CAST(SUBSTRING(id FROM '[0-9]+$') AS INTEGER) > " + CHANGESETS_BEFORE_FOREIGN_KEYS);
|
||||
migrate(connection, null);
|
||||
assertThat(foreignKeys(statement)).isEqualTo(foreignKeys);
|
||||
assertThat(rows(statement, "SELECT CAST(COUNT(*) AS TEXT) FROM qqq_fk_quarantine")).containsExactly("5");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run the changelog (all of it, or the first n changesets).
|
||||
*******************************************************************************/
|
||||
private static void migrate(Connection connection, Integer count) throws Exception
|
||||
{
|
||||
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
|
||||
Liquibase liquibase = new Liquibase(WebPerfSchema.CHANGELOG, new ClassLoaderResourceAccessor(), database);
|
||||
liquibase.setChangeLogParameter(WebPerfSchema.CHANGELOG_PREFIX_PROPERTY, PREFIX);
|
||||
if(count == null)
|
||||
{
|
||||
liquibase.update(new Contexts(), new LabelExpression());
|
||||
}
|
||||
else
|
||||
{
|
||||
liquibase.update(count, new Contexts(), new LabelExpression());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "child.column" for every table possible-value field and join of the QBit.
|
||||
*******************************************************************************/
|
||||
private static List<String> references() throws Exception
|
||||
{
|
||||
QInstance qInstance = WebPerfTestSupport.newInstance();
|
||||
new WebPerfQBitProducer().withQBitConfig(WebPerfTestSupport.config().withTableNamePrefix(PREFIX)).produce(qInstance);
|
||||
|
||||
List<String> references = new ArrayList<>();
|
||||
for(QTableMetaData table : qInstance.getTables().values())
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if(field.getPossibleValueSourceName() != null && qInstance.getTable(field.getPossibleValueSourceName()) != null)
|
||||
{
|
||||
references.add(table.getName() + "." + field.getBackendName());
|
||||
}
|
||||
}
|
||||
}
|
||||
for(QJoinMetaData join : qInstance.getJoins().values())
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// the referencing side is the one whose field is not its key //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData left = qInstance.getTable(join.getLeftTable());
|
||||
QTableMetaData right = qInstance.getTable(join.getRightTable());
|
||||
join.getJoinOns().forEach(on -> references.add(on.getRightField().equals(right.getPrimaryKeyField())
|
||||
? left.getName() + "." + left.getField(on.getLeftField()).getBackendName()
|
||||
: right.getName() + "." + right.getField(on.getRightField()).getBackendName()));
|
||||
}
|
||||
assertThat(references).as("the QBit's references").hasSizeGreaterThanOrEqualTo(12);
|
||||
return (references);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** "child.column" -> "parent RULE" for every foreign key in the database.
|
||||
*******************************************************************************/
|
||||
private static Map<String, String> foreignKeys(Statement statement) throws Exception
|
||||
{
|
||||
Map<String, String> foreignKeys = new HashMap<>();
|
||||
try(ResultSet resultSet = statement.executeQuery("""
|
||||
SELECT tc.table_name || '.' || kcu.column_name, ccu.table_name || ' ' || rc.delete_rule
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu ON kcu.constraint_name = tc.constraint_name
|
||||
JOIN information_schema.referential_constraints rc ON rc.constraint_name = tc.constraint_name
|
||||
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'"""))
|
||||
{
|
||||
while(resultSet.next())
|
||||
{
|
||||
foreignKeys.put(resultSet.getString(1), resultSet.getString(2));
|
||||
}
|
||||
}
|
||||
return (foreignKeys);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The first column of each row of a query, as text.
|
||||
*******************************************************************************/
|
||||
private static List<String> rows(Statement statement, String sql) throws Exception
|
||||
{
|
||||
List<String> rows = new ArrayList<>();
|
||||
try(ResultSet resultSet = statement.executeQuery(sql))
|
||||
{
|
||||
while(resultSet.next())
|
||||
{
|
||||
rows.add(resultSet.getString(1));
|
||||
}
|
||||
}
|
||||
return (rows);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qbits.webperf;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundleEntry;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.help.bundles.HelpBundles;
|
||||
import com.kingsrook.qqq.backend.core.testing.HelpBundleAssertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate the shipped bundle against the QBit's own metadata, without a host.
|
||||
*******************************************************************************/
|
||||
class WebPerfHelpBundleTest
|
||||
{
|
||||
/*******************************************************************************
|
||||
** All default features are present; registration needs no helpContent table.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void bundleCoversOwnedMetadata() throws Exception
|
||||
{
|
||||
QInstance qInstance = WebPerfTestSupport.newInstance();
|
||||
new WebPerfQBitProducer().withQBitConfig(WebPerfTestSupport.config()).produce(qInstance);
|
||||
|
||||
assertThat(qInstance.getTable("helpContent")).isNull();
|
||||
assertThat(HelpBundles.of(qInstance).getBundles()).singleElement()
|
||||
.satisfies(bundle ->
|
||||
{
|
||||
assertThat(bundle.getId()).isEqualTo("qbit-web-performance");
|
||||
assertThat(System.getProperty("qbit.artifactVersion")).isNotBlank();
|
||||
assertThat(bundle.getVersion()).isEqualTo(System.getProperty("qbit.artifactVersion"))
|
||||
.isEqualTo(WebPerfQBitProducer.VERSION);
|
||||
});
|
||||
HelpBundleAssertions.forInstance(qInstance)
|
||||
.bundles("help/qbit-web-performance.yaml")
|
||||
.owning(target -> target.name().startsWith("web_performance_") || target.name().startsWith("webPerformance"))
|
||||
.assertComplete();
|
||||
|
||||
// RC.7 validates supplied widget and lookup keys, but does not require their coverage.
|
||||
var keys = HelpBundles.of(qInstance).getBundles().get(0).getEntries().stream().map(HelpBundleEntry::getKey).toList();
|
||||
qInstance.getWidgets().keySet().stream().filter(name -> name.startsWith("webPerformance"))
|
||||
.forEach(name -> assertThat(keys).contains("widget:" + name + ";slot:label"));
|
||||
qInstance.getPossibleValueSources().keySet().stream().filter(name -> name.startsWith("web_performance_"))
|
||||
.forEach(name -> assertThat(keys).contains("possibleValueSource:" + name + ";slot:help"));
|
||||
}
|
||||
}
|
||||
@@ -86,71 +86,6 @@ class WebPerfP1Test
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The business home has no provider setup readout and no run-queue widget;
|
||||
** those are the admin-placeable list.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testHomeIsBusinessOnly()
|
||||
{
|
||||
List<String> home = qInstance.getApp(config.getAppName()).getWidgets();
|
||||
assertThat(home).doesNotContain(config.qualify(WebPerfWidgets.SETUP), config.qualify(WebPerfWidgets.RECENT_RUNS));
|
||||
assertThat(WebPerfWidgets.HOME).doesNotContain(WebPerfWidgets.SETUP, WebPerfWidgets.RECENT_RUNS);
|
||||
assertThat(WebPerfWidgets.ADMIN).containsExactly(WebPerfWidgets.SETUP, WebPerfWidgets.RECENT_RUNS);
|
||||
assertThat(home).contains(config.qualify(WebPerfWidgets.SITE_SCORES), config.qualify(WebPerfWidgets.TREND), config.qualify(WebPerfWidgets.SLOWEST_PAGES),
|
||||
config.qualify(WebPerfWidgets.OPPORTUNITIES), config.qualify(WebPerfWidgets.FIELD_STATUS));
|
||||
for(String name : WebPerfWidgets.ADMIN)
|
||||
{
|
||||
assertThat(qInstance.getWidget(config.qualify(name))).as(name + " is still defined for a host to place").isNotNull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unconfigured: a business empty state naming no config key. Configured:
|
||||
** nothing at all.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testNotConfiguredEmptyState() throws Exception
|
||||
{
|
||||
assertThat(html(WebPerfWidgets.NOT_CONFIGURED, Map.of())).as("configured shows nothing").isEmpty();
|
||||
|
||||
config.withLabRunner(null).withCruxClient(null);
|
||||
String card = html(WebPerfWidgets.NOT_CONFIGURED, Map.of());
|
||||
assertThat(card).contains("Page speed testing is not set up").contains("Ask an admin to add a Google PageSpeed API key.");
|
||||
assertThat(card).doesNotContain("googleApiKey").doesNotContain("qbit config").doesNotContain("environment variable");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The admin widgets render for an admin, and are hidden (permission denied
|
||||
** under enforcement) for a session without the admin permission.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testAdminWidgetsAdminOnly() throws Exception
|
||||
{
|
||||
for(String name : WebPerfWidgets.ADMIN)
|
||||
{
|
||||
assertThat(html(name, Map.of())).as(name + " for an admin").isNotEmpty();
|
||||
}
|
||||
|
||||
WebPerfTestSupport.login(qInstance, "web_performance.read", "web_performance.write", "web_performance.view.hasAccess");
|
||||
for(String name : WebPerfWidgets.ADMIN)
|
||||
{
|
||||
assertThat(qInstance.getWidget(config.qualify(name)).getPermissionRules().getDenyBehavior()).isEqualTo(com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior.HIDDEN);
|
||||
RenderWidgetInput input = new RenderWidgetInput().withWidgetMetaData(qInstance.getWidget(config.qualify(name)));
|
||||
input.setPermissionEnforcement(com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement.ENFORCE);
|
||||
org.junit.jupiter.api.Assertions.assertThrows(com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException.class,
|
||||
() -> new RenderWidgetAction().execute(input), name + " must not render without admin");
|
||||
assertThat(com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper.hasWidgetPermission(input, config.qualify(name))).isFalse();
|
||||
}
|
||||
assertThat(html(WebPerfWidgets.SITE_SCORES, Map.of())).as("business widgets still render").isNotEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An instance with a content table "page", the QBit wired to it, and the
|
||||
** local Google stand-in.
|
||||
@@ -271,73 +206,6 @@ class WebPerfP1Test
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** An odd daily cap rotates individual device strategies, not just pages.
|
||||
** Skipped rows and repeated scheduling must not starve the remaining pairs.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void scheduledQuotaRotatesPagesAndStrategies() throws Exception
|
||||
{
|
||||
config.withMaxRunsPerDay(3);
|
||||
Instant dayOne = Instant.parse("2026-10-19T12:00:00Z");
|
||||
WebPerfService service = service();
|
||||
service.applySync(service.planSync(sitemap), dayOne);
|
||||
WebPerfService.QueuePlan first = service.enqueue(service.scheduledRequests(false, "audit", dayOne), dayOne);
|
||||
assertEquals(3, first.queue().size());
|
||||
assertEquals(3, first.overQuota().size());
|
||||
assertEquals(0, service.enqueue(service.scheduledRequests(false, "audit", dayOne), dayOne).queue().size());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
|
||||
Instant dayTwo = dayOne.plusSeconds(86400);
|
||||
WebPerfService.QueuePlan second = service.enqueue(service.scheduledRequests(false, "audit", dayTwo), dayTwo);
|
||||
assertEquals(3, second.queue().size());
|
||||
List<String> firstPairs = first.queue().stream().map(run -> run.targetId() + "|" + run.strategy()).toList();
|
||||
assertThat(second.queue().stream().map(run -> run.targetId() + "|" + run.strategy()).toList())
|
||||
.doesNotContainAnyElementsOf(firstPairs);
|
||||
assertEquals(6, store().query("SELECT DISTINCT target_id, strategy FROM {lab_run} WHERE status <> 'SKIPPED_QUOTA'").size());
|
||||
assertThat(service.scheduledRequests(true, "key pages", dayTwo)).hasSize(2)
|
||||
.allSatisfy(run -> assertEquals(SITE + "/", run.url()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Manual and key-page runs consume the same allowance as the full audit.
|
||||
** Persisted history rotates the remaining pairs after a service restart.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void scheduledQuotaSharesAllowanceAndSurvivesRestart() throws Exception
|
||||
{
|
||||
config.withMaxRunsPerDay(3);
|
||||
Instant day = Instant.parse("2026-10-19T12:00:00Z");
|
||||
WebPerfService service = service();
|
||||
service.applySync(service.planSync(sitemap), day);
|
||||
List<WebPerfService.RunRequest> manual = service.requestsFor(service.enabledTargets(false), "MANUAL", "selected page", day);
|
||||
assertEquals(1, service.enqueue(List.of(manual.get(3)), day).queue().size());
|
||||
assertEquals(2, service.enqueue(service.scheduledRequests(true, "key pages", day), day).queue().size());
|
||||
assertEquals(0, service.enqueue(service.scheduledRequests(false, "audit", day), day).queue().size());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
|
||||
List<String> rotated = new ArrayList<>();
|
||||
for(int nextDay = 1; nextDay <= 3; nextDay++)
|
||||
{
|
||||
Instant now = day.plusSeconds(86400L * nextDay);
|
||||
WebPerfService restarted = new WebPerfService(new WebPerfRuntime(config), store());
|
||||
assertEquals(2, restarted.enqueue(restarted.scheduledRequests(true, "key pages", now), now).queue().size());
|
||||
WebPerfService.QueuePlan audit = restarted.enqueue(restarted.scheduledRequests(false, "audit", now), now);
|
||||
assertEquals(1, audit.queue().size());
|
||||
assertEquals(2, audit.merged().size());
|
||||
WebPerfService.RunRequest queued = audit.queue().get(0);
|
||||
assertThat(queued.url()).isNotEqualTo(SITE + "/");
|
||||
rotated.add(queued.targetId() + "|" + queued.strategy());
|
||||
store().update("UPDATE {lab_run} SET status = 'SUCCEEDED' WHERE status = 'QUEUED'");
|
||||
}
|
||||
assertThat(rotated).doesNotHaveDuplicates();
|
||||
assertEquals(6, store().query("SELECT DISTINCT target_id, strategy FROM {lab_run} WHERE status <> 'SKIPPED_QUOTA'").size());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The Setup card reports pages, budgets, the lab runner and field data from
|
||||
** the runtime's own resolution, with the next step for each.
|
||||
@@ -379,8 +247,7 @@ class WebPerfP1Test
|
||||
store().update("DELETE FROM {budget}");
|
||||
config.withReportRetentionDays(days);
|
||||
google.respond(new FixtureServer.Response(200, FixtureServer.fixture("psi-kof22-home-mobile.json")));
|
||||
long oldRunId = store().insert("lab_run", new java.util.LinkedHashMap<>(Map.of("status", "FAILED")));
|
||||
store().insert("lab_report", new java.util.LinkedHashMap<>(Map.of("run_id", oldRunId, "bytes", 1,
|
||||
store().insert("lab_report", new java.util.LinkedHashMap<>(Map.of("run_id", 999_999L, "bytes", 1,
|
||||
"create_date", Instant.now().minus(java.time.Duration.ofDays(40)))));
|
||||
WebPerfRuntime.of(qInstance, "web_performance_").allowReportPruneNow();
|
||||
WebPerfService service = service();
|
||||
@@ -390,8 +257,8 @@ class WebPerfP1Test
|
||||
|
||||
assertEquals(2, store().query("SELECT id FROM {lab_run} WHERE status = 'SUCCEEDED'").size(), "runs are untouched by pruning");
|
||||
assertThat(store().query("SELECT id FROM {lab_finding}")).as("findings are untouched by pruning").hasSizeGreaterThan(5);
|
||||
long old = store().query("SELECT id FROM {lab_report} WHERE run_id = ?", oldRunId).size();
|
||||
long fresh = store().query("SELECT id FROM {lab_report} WHERE run_id <> ?", oldRunId).size();
|
||||
long old = store().query("SELECT id FROM {lab_report} WHERE run_id = 999999").size();
|
||||
long fresh = store().query("SELECT id FROM {lab_report} WHERE run_id <> 999999").size();
|
||||
assertEquals(2, fresh, "the new reports are kept");
|
||||
assertEquals(days == 0 ? 1 : 0, old, "retention " + days + " days");
|
||||
}
|
||||
|
||||
@@ -74,9 +74,6 @@ class WebPerfQBitProducerTest
|
||||
@Test
|
||||
void testRegistersIntoAFreshInstance() throws Exception
|
||||
{
|
||||
assertThat(System.getProperty("qbit.artifactVersion")).isNotBlank();
|
||||
assertThat(WebPerfQBitProducer.VERSION).isEqualTo(System.getProperty("qbit.artifactVersion"));
|
||||
assertThat(new WebPerfQBitProducer().getQBitMetaData().getVersion()).isEqualTo(WebPerfQBitProducer.VERSION);
|
||||
QInstance qInstance = WebPerfTestSupport.newInstance();
|
||||
new WebPerfQBitProducer().withQBitConfig(WebPerfTestSupport.config()
|
||||
.withUrlSource(new SitemapUrlSource("https://www.example.com/sitemap.xml"))
|
||||
|
||||
@@ -127,18 +127,9 @@ public final class WebPerfTestSupport
|
||||
|
||||
List<String> statements = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean skip = false;
|
||||
for(String line : script.split("\n"))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// a changeset limited to other databases (the dbms:postgresql //
|
||||
// orphan cleanup) is skipped, as Liquibase skips it on H2 //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
if(line.startsWith("-- changeset "))
|
||||
{
|
||||
skip = line.contains(" dbms:") && !line.contains("h2");
|
||||
}
|
||||
if(skip || line.trim().startsWith("--"))
|
||||
if(line.trim().startsWith("--"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user