Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
724a126
feat: add /v1/geo-breakdown endpoint for geographic CWV breakdown (#94)
alonkochba Mar 23, 2026
6351949
test: add tests for /v1/geo-breakdown
max-ostapenko Mar 23, 2026
1789b9e
fix: update CDN cache duration in setCommonHeaders function
max-ostapenko Mar 24, 2026
3b751a4
fix: update CDN cache tag and duration in response headers
max-ostapenko Mar 24, 2026
0f7fe75
feat: add ETag support for caching in report responses
max-ostapenko Mar 24, 2026
e210b14
test: add ETag header tests for /v1/technologies and /v1/adoption routes
max-ostapenko Mar 24, 2026
de409d3
Merge branch 'main' into development
max-ostapenko Mar 24, 2026
553a80e
feat: implement CWV distribution endpoint with BigQuery integration a…
max-ostapenko Mar 27, 2026
6c18a4f
fix: remove unnecessary useLegacySql option from BigQuery query options
max-ostapenko Mar 30, 2026
72f5100
Merge remote-tracking branch 'origin/main' into development
max-ostapenko Mar 30, 2026
a3af58d
Merge branch 'main' into development
max-ostapenko Apr 6, 2026
22b7b44
feat: add workflow for testing
max-ostapenko Apr 6, 2026
a8d033f
fix: update ingress_settings default value to allow all traffic
max-ostapenko Apr 6, 2026
31e5765
feat: add geo filter support to CWV distribution endpoint and update …
max-ostapenko Apr 9, 2026
ceda97d
feat: add geo breakdown endpoint to readme and CWV distribution to MC…
max-ostapenko Apr 9, 2026
03e052e
feat: update CWV distribution query to handle 'ALL' technology case a…
max-ostapenko Apr 13, 2026
0301339
feat: configure ingress settings and increase Cloud Run service resou…
max-ostapenko Apr 15, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
jobs:
test:
runs-on: ubuntu-latest
if: github.head_ref != 'development'
steps:
- uses: actions/checkout@v6
- run: |
Expand Down
93 changes: 90 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,52 @@ curl --request GET \
]
```

### `GET /cwv-distribution`

Provides per-bucket CWV metric distribution histograms for technologies, optionally filtered by geo and rank.

#### CWV Distribution Parameters

- `technology` (required): Technology name(s) - comma-separated list, e.g. `Wix,WordPress`
- `date` (required): Crawl date in `YYYY-MM-DD` format, e.g. `2026-02-01`
- `geo` (optional): Geographic filter (defaults to `ALL`). Use a country name such as `United States of America` for country-level data.
- `rank` (optional): Numeric rank ceiling, e.g. `10000`. Omit or set to `ALL` to include all ranks.

#### CWV Distribution Response

```bash
curl --request GET \
--url 'https://{{HOST}}/v1/cwv-distribution?technology=WordPress&date=2026-02-01&geo=ALL'
```

Returns a JSON array where each element represents one histogram bucket for a technology/client/geo combination:

```json
[
{
"geo": "ALL",
"client": "mobile",
"technology": "WordPress",
"loading_bucket": 0,
"inp_bucket": 0,
"cls_bucket": 0,
"lcp_origins": 12345,
"inp_origins": 23456,
"cls_origins": 34567,
"fcp_origins": 11111,
"ttfb_origins": 22222
},
...
]
```

Bucket semantics:

- `loading_bucket` / `lcp_bucket` / `fcp_bucket` / `ttfb_bucket`: millisecond value (0–10000 in steps of 100)
- `inp_bucket`: `loading_bucket / 4` (INP scale)
- `cls_bucket`: `loading_bucket / 2000` (CLS scale)
- `*_origins`: count of distinct origins whose p75 value equals that bucket

### `GET /lighthouse`

Provides Lighthouse scores for technologies.
Expand Down Expand Up @@ -386,7 +432,6 @@ Returns a JSON object with the following schema:
]
```


### `GET /audits`

Provides Lighthouse audits for technologies.
Expand Down Expand Up @@ -450,6 +495,50 @@ Returns a JSON object with the following schema:
]
```

### `GET /geo-breakdown`

Provides Core Web Vitals breakdown by geography for a given technology and rank. Returns a single month snapshot of CWV data (LCP, CLS, INP, TTFB) across all geographies.

#### Geo Breakdown Parameters

- `technology` (optional): Technology name(s) - comma-separated list (defaults to `ALL`)
- `rank` (optional): Traffic rank segment, e.g. `top 1000`, `top 10000`. Defaults to `ALL`.
- `end` (optional): Snapshot date in `YYYY-MM-DD` format. Defaults to the latest available date.

#### Geo Breakdown Response

```bash
curl --request GET \
--url 'https://{{HOST}}/v1/geo-breakdown?technology=WordPress&rank=top%2010000'
```

Returns a JSON array where each element represents CWV data for a technology on a given date and geographic region:

```json
[
{
"date": "2026-02-01",
"geo": "United States of America",
"technology": "WordPress",
"vitals": [
{
"mobile": {
"good_number": 12345,
"tested": 56789
},
"desktop": {
"good_number": 6789,
"tested": 10000
},
"name": "lcp"
},
...
]
},
...
]
```

### `GET /ranks`

Lists all available ranks.
Expand Down Expand Up @@ -663,5 +752,3 @@ Response:
...
}
```


52 changes: 52 additions & 0 deletions src/controllers/cwvDistributionController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { queryCWVDistribution } from '../utils/reportService.js';
import {
handleControllerError,
generateETag,
isModified,
sendValidationError
} from '../utils/controllerHelpers.js';

/**
* GET /v1/cwv-distribution
*
* Query parameters:
* technology (required) - comma-separated list of technologies, e.g. "Wix,WordPress"
* date (required) - crawl date in YYYY-MM-DD format, e.g. "2026-02-01"
* rank (optional) - numeric rank ceiling, e.g. "10000". Omit or set to "ALL" to include all ranks.
* geo (optional) - geographic filter, e.g. "United States of America". Defaults to "ALL".
*/
export const listCWVDistributionData = async (req, res) => {
try {
const params = req.query;

const errors = [];
if (!params.technology) errors.push(['technology', 'missing technology parameter']);
if (!params.date) errors.push(['date', 'missing date parameter']);
if (errors.length > 0) {
sendValidationError(res, errors);
return;
}

const rows = await queryCWVDistribution({
technology: params.technology,
date: params.date,
geo: params.geo || 'ALL',
rank: params.rank && params.rank !== 'ALL' ? params.rank : null,
});

const jsonData = JSON.stringify(rows);
const etag = generateETag(jsonData);
res.setHeader('ETag', `"${etag}"`);
if (!isModified(req, etag)) {
res.statusCode = 304;
res.end();
return;
}

res.statusCode = 200;
res.end(jsonData);

} catch (error) {
handleControllerError(res, error, 'fetching CWV distribution data');
}
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const CONTROLLER_MODULES = {
geos: './controllers/geosController.js',
versions: './controllers/versionsController.js',
static: './controllers/cdnController.js',
cwvDistribution: './controllers/cwvDistributionController.js',
};

const controllers = {};
Expand All @@ -37,6 +38,7 @@ const V1_ROUTES = {
'/v1/geos': ['geos', 'listGeos'],
'/v1/versions': ['versions', 'listVersions'],
'/v1/geo-breakdown': ['geoBreakdown', 'listGeoBreakdownData'],
'/v1/cwv-distribution': ['cwvDistribution', 'listCWVDistributionData']
};

// Helper function to set CORS headers
Expand Down
16 changes: 16 additions & 0 deletions src/mcpHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
queryRanks,
queryGeos,
queryVersions,
queryCWVDistribution,
} from './utils/reportService.js';

const createMcpServer = () => {
Expand Down Expand Up @@ -140,6 +141,21 @@ const createMcpServer = () => {
}
);

server.tool(
'get_cwv_distribution',
'Get Core Web Vitals metric distribution histograms for websites using specific web technologies. Returns per-bucket origin counts for LCP, INP, CLS, FCP, and TTFB, optionally filtered by geography and rank.',
{
technology: z.string().describe('Comma-separated technology names (e.g. "WordPress" or "Wix,WordPress")'),
date: z.string().describe('Crawl date in YYYY-MM-DD format (e.g. "2026-02-01")'),
geo: z.string().optional().describe('Geographic filter — a country name (e.g. "United States of America") or "ALL" for global data. Defaults to "ALL"'),
rank: z.string().optional().describe('Numeric rank ceiling (e.g. "10000"). Omit or set to "ALL" for all ranks'),
},
async ({ technology, date, geo, rank }) => {
const data = await queryCWVDistribution({ technology, date, geo: geo || 'ALL', rank: rank && rank !== 'ALL' ? rank : null });
return { content: [{ type: 'text', text: JSON.stringify(data) }] };
}
);

server.tool(
'list_ranks',
'List available traffic rank segments for filtering Tech Report data (e.g. "top 1000", "top 10000", "top 100000", "ALL").',
Expand Down
87 changes: 87 additions & 0 deletions src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
"node": ">=22.0.0"
},
"scripts": {
"start": "DATABASE=tech-report-api-prod functions-framework --target=app",
"function": "DATABASE=tech-report-api-prod functions-framework --target=app",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:live": "bash ../test-api.sh",
"build": "docker build -t report-api .",
"run": "docker run -p 8080:8080 report-api"
"docker": "docker run -p 8080:8080 report-api"
},
"dependencies": {
"@google-cloud/bigquery": "^7.9.1",
"@google-cloud/firestore": "8.3.0",
"@google-cloud/functions-framework": "^5.0.2",
"@google-cloud/storage": "7.19.0",
Expand Down
Loading