Skip to content

Commit

Permalink
Merge pull request #6 from treosh/v3
Browse files Browse the repository at this point in the history
v3: new metrics & history API
  • Loading branch information
alekseykulikov authored Oct 15, 2023
2 parents 3bc9127 + e6f2b9a commit 444eff0
Show file tree
Hide file tree
Showing 11 changed files with 2,042 additions and 7,289 deletions.
14 changes: 6 additions & 8 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use node 12
uses: actions/setup-node@v1
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: '12.x'
- name: Install deps
run: yarn install
- name: Test
run: yarn test
node-version: '18'
cache: 'yarn'
- run: yarn install
- run: yarn test
env:
CRUX_KEY: ${{ secrets.CRUX_KEY }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
dist
results
node_modules
892 changes: 822 additions & 70 deletions README.md

Large diffs are not rendered by default.

57 changes: 23 additions & 34 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,66 +1,55 @@
{
"name": "crux-api",
"version": "2.0.0",
"description": "A Chrome UX Report API wrapper wrapper that handles errors and provides types",
"version": "3.0.0",
"description": "A tiny CrUX API wrapper that supports record & history API, handles errors, and provides types.",
"repository": "https://github.com/treosh/crux-api",
"bugs": "https://github.com/treosh/crux-api/issues",
"license": "MIT",
"type": "module",
"source": "src/index.js",
"module": "src/index.js",
"types": "dist/crux-api.d.ts",
"main": "dist/crux-api.js",
"types": "types/index.d.ts",
"files": [
"dist",
"src"
"src",
"types"
],
"scripts": {
"build": "run-s build:*",
"build:src": "microbundle build --no-sourcemap --format=cjs",
"build:src-ts": "tsc --declaration --noEmit false --outDir dist/ --allowJs src/index.js && rm dist/index.js && mv dist/index.d.ts dist/crux-api.d.ts",
"test": "ava -v && prettier -c src test script README.md && yarn build && tsc -p . && size-limit",
"prepack": "yarn build"
"types": "tsc --declaration --emitDeclarationOnly --noEmit false --outDir types/ --allowJs src/index.js",
"test": "yarn types && ava -v test/index.js && prettier -c src test script README.md && tsc -p . && size-limit"
},
"devDependencies": {
"@size-limit/preset-small-lib": "^4.11.0",
"@types/node-fetch": "^2.5.10",
"ava": "^3.15.0",
"esm": "^3.2.25",
"microbundle": "^0.13.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.3.1",
"size-limit": "^4.11.0",
"typescript": "^4.3.2"
"@size-limit/preset-small-lib": "^9.0.0",
"@types/node": "^20.8.6",
"ava": "^5.3.1",
"node-fetch": "^3.3.2",
"prettier": "^3.0.3",
"size-limit": "^9.0.0",
"typescript": "^5.2.2"
},
"keywords": [
"CrUX",
"CrUX API",
"CrUX History API",
"Chrome User Experience Report",
"Chrome UX Report",
"Core Web Vitals",
"Web Performance",
"records.queryRecord",
"FCP",
"First Contentful Paint",
"LCP",
"Largest Contentful Paint",
"FID",
"First Input Delay",
"CLS",
"Cumulative Layout Shift"
"Cumulative Layout Shift",
"INP",
"Interation to Next Paint",
"TTFB",
"Time to First Byte"
],
"size-limit": [
{
"limit": "450B",
"limit": "510B",
"path": "./src/index.js"
}
],
"ava": {
"require": [
"esm"
],
"files": [
"test/*.js"
]
}
]
}
19 changes: 19 additions & 0 deletions script/crux-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// usage:
// • CRUX_KEY='...' node script/crux-api.js record '{"origin": "https://example.com", "formFactor": "DESKTOP", "effectiveConnectionType": "4G" }' > results/record-example.json
// • CRUX_KEY='...' node script/crux-api.js history '{"url": "https://www.apple.com/", "formFactor": "PHONE" }' > results/history-apple.json

import nodeFetch from 'node-fetch'
import { createQueryRecord, createQueryHistoryRecord } from '../src/index.js'

const key = process.env.CRUX_KEY || 'no-key'
const apiMethod = process.argv[2] || 'record'
const params = JSON.parse(process.argv[3] || '')

try {
const createMethod = apiMethod === 'record' ? createQueryRecord : createQueryHistoryRecord
const queryMethod = createMethod({ key, fetch: nodeFetch })
const res = await queryMethod(params)
console.log(JSON.stringify(res, null, ' '))
} catch (err) {
console.error(err)
}
14 changes: 0 additions & 14 deletions script/queryRecord.js

This file was deleted.

65 changes: 55 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const maxRetryTimeout = 60 * 1000 // 60s
* @typedef {'ALL_FORM_FACTORS' | 'PHONE' | 'DESKTOP' | 'TABLET'} FormFactor
* @typedef {'4G' | '3G' | '2G' | 'slow-2G' | 'offline'} Connection
* @typedef {{ histogram: { start: number | string, end: number | string, density: number }[], percentiles: { p75: number | string } }} MetricValue
* @typedef {'first_contentful_paint' | 'largest_contentful_paint' | 'first_input_delay' | 'cumulative_layout_shift'} MetricName
* @typedef {{ year: number, month: number, day: number }} MetricDate
* @typedef {{ firstDate: MetricDate, lastDate: MetricDate }} CollectionPeriod
* @typedef {{ error: { code: number, message: string, status: string } }} ErrorResponse
* @typedef {{
* record: {
Expand All @@ -22,46 +23,90 @@ const maxRetryTimeout = 60 * 1000 // 60s
* largest_contentful_paint?: MetricValue,
* first_input_delay?: MetricValue,
* cumulative_layout_shift?: MetricValue,
* }
* interaction_to_next_paint?: MetricValue,
* experimental_time_to_first_byte?: MetricValue,
* },
* collectionPeriod: CollectionPeriod
* },
* urlNormalizationDetails?: {
* originalUrl: string,
* normalizedUrl: string
* }
* }} SuccessResponse
*
* @typedef {(?number | string)[]} PercentileValues
* @typedef {{ start: number, end?: number, densities: (number | 'NaN')[] }} HistorgramTimeserie
* @typedef {{
* histogramTimeseries: HistorgramTimeserie[],
* percentilesTimeseries: { p75s: PercentileValues }
* }} HistoryValue
*
* @typedef {{
* record: {
* key: {
* url?: string,
* origin?: string,
* formFactor?: FormFactor
* },
* metrics: {
* first_input_delay?: HistoryValue,
* first_contentful_paint?: HistoryValue,
* largest_contentful_paint?: HistoryValue,
* cumulative_layout_shift?: HistoryValue,
* interaction_to_next_paint?: HistoryValue,
* experimental_time_to_first_byte?: HistoryValue,
* }
* collectionPeriods: CollectionPeriod[]
* },
* urlNormalizationDetails?: {
* originalUrl: string,
* normalizedUrl: string
* },
* }} HistoryResponse
*/

/** @param {CreateOptions} createOptions @return {function(QueryRecordOptions): Promise<SuccessResponse | null>} */
export function createQueryRecord(createOptions) {
return createQueryCruxApi({ ...createOptions, api: 'record' })
}

/** @param {CreateOptions} createOptions @return {function(QueryRecordOptions): Promise<HistoryResponse | null>} */
export function createQueryHistoryRecord(createOptions) {
return createQueryCruxApi({ ...createOptions, api: 'history' })
}

/**
* Fetch CrUX API and handles 4xx errors.
* Inspired by: https://github.com/GoogleChrome/CrUX/blob/master/js/crux-api-util.js
*
* @param {CreateOptions} createOptions
* @param {CreateOptions & { api: 'history' | 'record' }} createOptions
*/

export function createQueryRecord(createOptions) {
function createQueryCruxApi(createOptions) {
const key = createOptions.key
const fetch = createOptions.fetch || window.fetch
return queryRecord
const apiMethod = createOptions.api === 'history' ? 'queryHistoryRecord' : 'queryRecord'
return queryCruxApi

/**
* @param {QueryRecordOptions} queryOptions
* @return {Promise<SuccessResponse | null>}
* @return {Promise<any | null>}
*/

async function queryRecord(queryOptions, retryCounter = 1) {
const apiEndpoint = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${key}`
async function queryCruxApi(queryOptions, retryCounter = 1) {
const apiEndpoint = `https://chromeuxreport.googleapis.com/v1/records:${apiMethod}?key=${key}`
const res = await fetch(apiEndpoint, { method: 'POST', body: JSON.stringify(queryOptions) })
if (res.status >= 500) throw new Error(`Invalid CrUX API status: ${res.status}`)

const json = await res.json()
if (json && json.error) {
const { error } = /** @type {ErrorResponse} */ (json)
if (error.code === 404) return null
if (error.code === 429) return retryAfterTimeout(retryCounter, () => queryRecord(queryOptions, retryCounter + 1))
if (error.code === 429) return retryAfterTimeout(retryCounter, () => queryCruxApi(queryOptions, retryCounter + 1))
throw new Error(JSON.stringify(error))
}
if (!json || (json && !json.record.key)) throw new Error(`Invalid response: ${JSON.stringify(json)}`)
return /** @type {SuccessResponse} */ (json)
return json
}
}

Expand Down
31 changes: 19 additions & 12 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
// yarn ava test/index.js
// usage: CRUX_KEY='...' yarn ava test/index.js

import test from 'ava'
import fetch from 'node-fetch'
import { createQueryRecord, normalizeUrl } from '../src'
import { createQueryRecord, createQueryHistoryRecord, normalizeUrl } from '../src/index.js'

const key = process.env.CRUX_KEY || 'no-key'

test('createQueryRecord', async (t) => {
const queryRecord = createQueryRecord({ key, fetch })
const json1 = await queryRecord({ url: 'https://github.com/', formFactor: 'DESKTOP' })
t.truthy(json1)
if (json1) {
t.is(json1.record.key.url, 'https://github.com/')
t.is(json1.record.key.formFactor, 'DESKTOP')
}

t.is(json1?.record.key.url, 'https://github.com/')
t.is(json1?.record.key.formFactor, 'DESKTOP')

const json2 = await queryRecord({ origin: 'https://github.com', effectiveConnectionType: '3G' })
t.truthy(json2)
if (json2) {
t.is(json2.record.key.origin, 'https://github.com')
t.is(json2.record.key.effectiveConnectionType, '3G')
}
t.is(json2?.record.key.origin, 'https://github.com')
t.is(json2?.record.key.effectiveConnectionType, '3G')
})

test('createQueryHistoryRecord', async (t) => {
const queryHistory = createQueryHistoryRecord({ key, fetch })
const json1 = await queryHistory({ url: 'https://github.com/', formFactor: 'DESKTOP' })

t.is(json1?.record.key.url, 'https://github.com/')
t.is(json1?.record.key.formFactor, 'DESKTOP')
t.is(json1?.record.collectionPeriods.length, 25)
t.is(json1?.record.metrics.cumulative_layout_shift?.histogramTimeseries.length, 3)
t.is(json1?.record.metrics.cumulative_layout_shift?.histogramTimeseries[0]?.densities.length, 25)
})

test('normalizeUrl', async (t) => {
Expand Down
5 changes: 3 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"compilerOptions": {
"noEmit": true,
"module": "CommonJS",
"target": "ES2020",
"moduleResolution": "node",
"module": "esnext",
"target": "esnext",
"esModuleInterop": true,
"allowJs": true,
"checkJs": true,
Expand Down
Loading

0 comments on commit 444eff0

Please sign in to comment.