Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
- "scripts/build-product-site.py"
- "scripts/validate-product-build.py"
- "scripts/submit-indexnow.py"
- "scripts/inject-site-measurement.py"
- "scripts/validate-site-measurement.py"
- "scripts/check-site-health.py"
- ".github/workflows/pages.yml"
pull_request:
branches: [ main ]
Expand All @@ -18,6 +21,9 @@ on:
- "scripts/build-product-site.py"
- "scripts/validate-product-build.py"
- "scripts/submit-indexnow.py"
- "scripts/inject-site-measurement.py"
- "scripts/validate-site-measurement.py"
- "scripts/check-site-health.py"
- ".github/workflows/pages.yml"
workflow_dispatch:

Expand Down Expand Up @@ -103,6 +109,19 @@ jobs:
- name: Build deterministic product website
run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json

- name: Configure optional client measurement
env:
GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }}
run: python scripts/inject-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID"

- name: Validate measurement contract
env:
GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }}
run: python scripts/validate-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID"

- name: Check internal links and fragments
run: python scripts/check-site-health.py --site _site --output _validation/site-health

- name: Validate IndexNow payload without network submission
run: python scripts/submit-indexnow.py --sitemap _site/sitemap.xml --dry-run

Expand All @@ -119,7 +138,9 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: product-build-validation
path: _validation/build.log
path: |
_validation/build.log
_validation/site-health/
if-no-files-found: warn

- name: Enforce rendered validation
Expand All @@ -136,6 +157,7 @@ jobs:
! grep -R --line-number --fixed-strings 'LICENSE-APACHE-2.0' _site
! grep -R --line-number -E '(href|src|content)="http://' _site --include='*.html'
! grep -R --line-number --fixed-strings 'github.com/ghraw/masarray/arsas/main/Assets/screenshot' _site --include='*.html'
! grep -R --line-number --fixed-strings '__ARSAS_GA4_MEASUREMENT_ID__' _site --include='*.html'

- name: Upload website artifact
if: github.event_name != 'pull_request'
Expand Down
199 changes: 199 additions & 0 deletions .github/workflows/site-measurement.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
name: Measure product website

on:
schedule:
- cron: "17 3 * * 1"
workflow_dispatch:
inputs:
days:
description: Aggregated reporting window in days
required: false
default: "28"
type: choice
options: ["7", "28", "60", "90"]
pull_request:
branches: [ main ]
paths:
- "landing/**"
- "scripts/build-product-site.py"
- "scripts/inject-site-measurement.py"
- "scripts/validate-site-measurement.py"
- "scripts/check-site-health.py"
- "scripts/build-site-measurement-report.py"
- ".github/workflows/site-measurement.yml"
- ".github/workflows/pages.yml"
push:
branches: [ main ]
paths:
- "landing/**"
- "scripts/build-product-site.py"
- "scripts/inject-site-measurement.py"
- "scripts/validate-site-measurement.py"
- "scripts/check-site-health.py"
- "scripts/build-site-measurement-report.py"
- ".github/workflows/site-measurement.yml"
- ".github/workflows/pages.yml"

permissions:
contents: read

concurrency:
group: site-measurement-${{ github.ref }}
cancel-in-progress: true

env:
CANONICAL_ROOT: https://masarray.github.io/arsas/

jobs:
quality:
name: Validate measurement and internal links
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
show-progress: false

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install offline report dependency
run: python -m pip install --disable-pip-version-check --quiet requests

- name: Prepare stable release evidence
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
cp landing/latest.json /tmp/arsas-published.json
else
gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json
fi

- name: Build deterministic website
run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json

- name: Configure optional client measurement
env:
GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }}
run: python scripts/inject-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID"

- name: Validate page, download, language, 404 and Web Vitals measurement
env:
GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }}
run: python scripts/validate-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID"

- name: Check internal links and fragments
id: local_health
continue-on-error: true
run: python scripts/check-site-health.py --site _site --output _measurement

- name: Exercise reporting without credentials or network data
env:
GOOGLE_SERVICE_ACCOUNT_JSON: ""
GA4_PROPERTY_ID: ""
GSC_SITE_URL: https://masarray.github.io/arsas/
PAGESPEED_URLS: ""
GITHUB_STEP_SUMMARY: ""
run: python scripts/build-site-measurement-report.py --site _site --output _measurement/offline --days 7

- name: Add local health summary
if: always()
shell: bash
run: |
if [ -f _measurement/site-health.md ]; then
cat _measurement/site-health.md >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload quality evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: site-measurement-quality
path: _measurement/
if-no-files-found: warn
retention-days: 30

- name: Enforce link health
if: steps.local_health.outcome != 'success'
run: exit 1

insights:
name: Build private traffic, search and Web Vitals report
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs: quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
show-progress: false

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install read-only reporting dependencies
run: python -m pip install --disable-pip-version-check --quiet google-auth requests

- name: Prepare stable release evidence
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json

- name: Build current website
run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json

- name: Configure current client measurement
env:
GA4_MEASUREMENT_ID: ${{ vars.GA4_MEASUREMENT_ID }}
run: python scripts/inject-site-measurement.py _site --measurement-id "$GA4_MEASUREMENT_ID"

- name: Check deployed pages, release assets and 404 response
id: deployed_health
continue-on-error: true
run: python scripts/check-site-health.py --site _site --output _measurement --remote --base-url "$CANONICAL_ROOT"

- name: Build aggregated measurement report
id: measurement
continue-on-error: true
env:
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
GA4_PROPERTY_ID: ${{ vars.GA4_PROPERTY_ID }}
GSC_SITE_URL: ${{ vars.GSC_SITE_URL || 'https://masarray.github.io/arsas/' }}
PAGESPEED_API_KEY: ${{ secrets.PAGESPEED_API_KEY }}
PAGESPEED_URLS: ${{ vars.PAGESPEED_URLS || 'https://masarray.github.io/arsas/,https://masarray.github.io/arsas/download.html,https://masarray.github.io/arsas/smart-reporting.html,https://masarray.github.io/arsas/guides.html,https://masarray.github.io/arsas/id.html,https://masarray.github.io/arsas/unduh.html' }}
REPORT_DAYS: ${{ inputs.days || '28' }}
shell: bash
run: python scripts/build-site-measurement-report.py --site _site --output _measurement --days "$REPORT_DAYS"

- name: Add deployed health summary
if: always()
shell: bash
run: |
if [ -f _measurement/site-health.md ]; then
cat _measurement/site-health.md >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload private measurement evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: site-measurement-${{ github.run_number }}
path: _measurement/
if-no-files-found: error
retention-days: 90

- name: Enforce measurement execution
if: steps.measurement.outcome != 'success'
run: exit 1

- name: Enforce deployed link and 404 health
if: steps.deployed_health.outcome != 'success'
run: exit 1
87 changes: 87 additions & 0 deletions docs/website-measurement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# ARSAS website measurement

ARSAS uses a lightweight, evidence-oriented measurement pipeline. Runtime analytics are optional and remain disabled when no valid measurement ID is configured. Search and performance reports are private GitHub Actions artifacts; they are not deployed to the public website.

## What is measured

| Question | Source | Output |
|---|---|---|
| Which pages are visited most? | GA4 `page_view` | Page path, views and active users |
| Which queries bring users? | Google Search Console | Query, clicks, impressions, CTR and average position |
| Which download buttons are clicked? | GA4 events | `download_installer`, `download_portable`, `download_checksums` |
| English vs Indonesian traffic | Page registry + GA4/Search Console | Views, search impressions and clicks by site language |
| Which pages have high impressions but low clicks? | Search Console | Pages and queries above the configured opportunity thresholds |
| Are links broken or 404s occurring? | Build-time crawler + deployed probe + GA4 | Missing files/fragments, HTTP failures, 404 paths and referrers |
| Are Core Web Vitals healthy? | Browser PerformanceObserver + PageSpeed/CrUX | LCP, CLS and INP field/lab evidence |

The browser client disables Google advertising signals, does not request ad-personalization signals, respects `Do Not Track`, loads asynchronously and performs no network request when the measurement ID is absent.

## Repository configuration

Configure these **Actions variables**:

- `GA4_MEASUREMENT_ID`: public web stream ID such as `G-XXXXXXXXXX`. Leaving it empty keeps client measurement disabled.
- `GA4_PROPERTY_ID`: numeric GA4 property ID used by the private reporting workflow.
- `GSC_SITE_URL`: the exact verified Search Console property, normally `https://masarray.github.io/arsas/` for a URL-prefix property.
- `PAGESPEED_URLS`: optional comma-separated URLs. When omitted, the workflow checks the English and Indonesian home/download pages plus Smart Reporting and Guides.

Configure these **Actions secrets**:

- `GOOGLE_SERVICE_ACCOUNT_JSON`: JSON for a service account with read-only access to the GA4 property and Search Console property. A `base64:<payload>` value is also accepted.
- `PAGESPEED_API_KEY`: optional PageSpeed Insights API key. The report still attempts the public endpoint when this secret is absent.

Grant the service account Viewer/read access only. It does not need permission to modify analytics, Search Console, releases or the website.

## Workflow behavior

`.github/workflows/site-measurement.yml` runs:

- on relevant pull requests and pushes: deterministic build, measurement contract validation, internal links and fragment validation;
- every Monday at 03:17 UTC, or manually: deployed page checks, official release-asset checks, an intentional 404 probe, GA4 aggregate reports, Search Console reports and PageSpeed/CrUX collection.

Artifacts:

- `site-measurement-quality`: local link and instrumentation evidence, retained for 30 days;
- `site-measurement-<run>`: private Markdown/JSON traffic, search, 404 and Core Web Vitals evidence, retained for 90 days.

The same Markdown report is written to the GitHub Actions job summary.

## Opportunity rules

The initial low-CTR queue is deliberately conservative:

- query opportunity: at least 50 impressions, CTR below 3%, average position 20 or better;
- page opportunity: at least 100 impressions, CTR below 3%, average position 20 or better.

These thresholds are implemented in `scripts/build-site-measurement-report.py` and can be adjusted after several reporting cycles establish a stable baseline.

## Event contract

The local `landing/analytics.js` client emits:

- `page_view`;
- `page_not_found`;
- `language_switch`;
- `download_installer`;
- `download_portable`;
- `download_checksums`;
- `web_vital_lcp`;
- `web_vital_cls`;
- `web_vital_inp`;
- diagnostic `web_vital_ttfb`.

Every event carries page path, page title, site language, content group and stable release version. Download events also carry the official file name, destination URL and visible link text.

## Interpreting Core Web Vitals

Browser RUM events provide continuous observations from measured visits. The scheduled PageSpeed report remains the decision source for field CWV because it uses CrUX data when enough real-user samples exist. When CrUX has insufficient traffic, the report retains Lighthouse lab values and marks field data unavailable instead of inventing a pass/fail result.

## Continuous-improvement loop

1. Review the weekly job summary.
2. Repair any broken internal link or failed 404 behavior immediately.
3. Prioritize high-impression pages with low CTR for title, description and intent alignment.
4. Compare English and Indonesian traffic before deciding which translations to expand.
5. Trace download clicks back to the page that generated them.
6. Investigate repeated 404 paths and add a valid route or redirect where appropriate.
7. Treat poor LCP, CLS or INP as a release-quality issue, then confirm the improvement in the next field-data cycle.
Loading
Loading