diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 41e4e5c2a3b3..7012d2ec937c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -200,6 +200,9 @@ jobs: changed_node: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/node') }} + changed_node_overhead_action: + ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, + '@sentry-internal/node-overhead-gh-action') }} changed_deno: ${{ needs.job_get_metadata.outputs.changed_ci == 'true' || contains(steps.checkForAffected.outputs.affected, '@sentry/deno') }} @@ -253,6 +256,37 @@ jobs: # Only run comparison against develop if this is a PR comparison_branch: ${{ (github.event_name == 'pull_request' && github.base_ref) || ''}} + job_node_overhead_check: + name: Node Overhead Check + needs: [job_get_metadata, job_build] + timeout-minutes: 15 + runs-on: ubuntu-24.04 + if: + (needs.job_build.outputs.changed_node == 'true' && github.event_name == 'pull_request') || + (needs.job_build.outputs.changed_node_overhead_action == 'true' && github.event_name == 'pull_request') || + needs.job_get_metadata.outputs.is_base_branch == 'true' || needs.job_get_metadata.outputs.is_release == 'true' + steps: + - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }}) + uses: actions/checkout@v4 + with: + ref: ${{ env.HEAD_COMMIT }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version-file: 'package.json' + - name: Restore caches + uses: ./.github/actions/restore-cache + with: + dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Check node overhead + uses: ./dev-packages/node-overhead-gh-action + env: + DEBUG: '1' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + # Only run comparison against develop if this is a PR + comparison_branch: ${{ (github.event_name == 'pull_request' && github.base_ref) || ''}} + job_lint: name: Lint # Even though the linter only checks source code, not built code, it needs the built code in order check that all diff --git a/dev-packages/node-overhead-gh-action/.eslintrc.cjs b/dev-packages/node-overhead-gh-action/.eslintrc.cjs new file mode 100644 index 000000000000..381653af6ece --- /dev/null +++ b/dev-packages/node-overhead-gh-action/.eslintrc.cjs @@ -0,0 +1,15 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['**/*.mjs'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + }, + ], +}; diff --git a/dev-packages/node-overhead-gh-action/README.md b/dev-packages/node-overhead-gh-action/README.md new file mode 100644 index 000000000000..1759ab7bd7c3 --- /dev/null +++ b/dev-packages/node-overhead-gh-action/README.md @@ -0,0 +1,3 @@ +# node-overhead-gh-action + +Capture the overhead of Sentry in a node app. diff --git a/dev-packages/node-overhead-gh-action/action.yml b/dev-packages/node-overhead-gh-action/action.yml new file mode 100644 index 000000000000..e90aef2e4342 --- /dev/null +++ b/dev-packages/node-overhead-gh-action/action.yml @@ -0,0 +1,17 @@ +name: 'node-overhead-gh-action' +description: 'Run node overhead comparison' +inputs: + github_token: + required: true + description: 'a github access token' + comparison_branch: + required: false + default: '' + description: 'If set, compare the current branch with this branch' + threshold: + required: false + default: '3' + description: 'The percentage threshold for size changes before posting a comment' +runs: + using: 'node24' + main: 'index.mjs' diff --git a/dev-packages/node-overhead-gh-action/index.mjs b/dev-packages/node-overhead-gh-action/index.mjs new file mode 100644 index 000000000000..0a0f02e41b9a --- /dev/null +++ b/dev-packages/node-overhead-gh-action/index.mjs @@ -0,0 +1,236 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { DefaultArtifactClient } from '@actions/artifact'; +import * as core from '@actions/core'; +import { exec } from '@actions/exec'; +import { context, getOctokit } from '@actions/github'; +import * as glob from '@actions/glob'; +import * as io from '@actions/io'; +import { markdownTable } from 'markdown-table'; +import { getArtifactsForBranchAndWorkflow } from './lib/getArtifactsForBranchAndWorkflow.mjs'; +import { getAveragedOverheadMeasurements } from './lib/getOverheadMeasurements.mjs'; +import { formatResults, hasChanges } from './lib/markdown-table-formatter.mjs'; + +const NODE_OVERHEAD_HEADING = '## node-overhead report 🧳'; +const ARTIFACT_NAME = 'node-overhead-action'; +const RESULTS_FILE = 'node-overhead-results.json'; + +function getResultsFilePath() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + return path.resolve(__dirname, RESULTS_FILE); +} + +const { getInput, setFailed } = core; + +async function fetchPreviousComment(octokit, repo, pr) { + const { data: commentList } = await octokit.rest.issues.listComments({ + ...repo, + issue_number: pr.number, + }); + + return commentList.find(comment => comment.body.startsWith(NODE_OVERHEAD_HEADING)); +} + +async function run() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + + try { + const { payload, repo } = context; + const pr = payload.pull_request; + + const comparisonBranch = getInput('comparison_branch'); + const githubToken = getInput('github_token'); + const threshold = getInput('threshold') || 1; + + if (comparisonBranch && !pr) { + throw new Error('No PR found. Only pull_request workflows are supported.'); + } + + const octokit = getOctokit(githubToken); + const resultsFilePath = getResultsFilePath(); + + // If we have no comparison branch, we just run overhead check & store the result as artifact + if (!comparisonBranch) { + return runNodeOverheadOnComparisonBranch(); + } + + // Else, we run overhead check for the current branch, AND fetch it for the comparison branch + let base; + let current; + let baseIsNotLatest = false; + let baseWorkflowRun; + + try { + const workflowName = `${process.env.GITHUB_WORKFLOW || ''}`; + core.startGroup(`getArtifactsForBranchAndWorkflow - workflow:"${workflowName}", branch:"${comparisonBranch}"`); + const artifacts = await getArtifactsForBranchAndWorkflow(octokit, { + ...repo, + artifactName: ARTIFACT_NAME, + branch: comparisonBranch, + workflowName, + }); + core.endGroup(); + + if (!artifacts) { + throw new Error('No artifacts found'); + } + + baseWorkflowRun = artifacts.workflowRun; + + await downloadOtherWorkflowArtifact(octokit, { + ...repo, + artifactName: ARTIFACT_NAME, + artifactId: artifacts.artifact.id, + downloadPath: __dirname, + }); + + base = JSON.parse(await fs.readFile(resultsFilePath, { encoding: 'utf8' })); + + if (!artifacts.isLatest) { + baseIsNotLatest = true; + core.info('Base artifact is not the latest one. This may lead to incorrect results.'); + } + } catch (error) { + core.startGroup('Warning, unable to find base results'); + core.error(error); + core.endGroup(); + } + + core.startGroup('Getting current overhead measurements'); + try { + current = await getAveragedOverheadMeasurements(); + } catch (error) { + core.error('Error getting current overhead measurements'); + core.endGroup(); + throw error; + } + core.debug(`Current overhead measurements: ${JSON.stringify(current, null, 2)}`); + core.endGroup(); + + const thresholdNumber = Number(threshold); + + const nodeOverheadComment = await fetchPreviousComment(octokit, repo, pr); + + if (nodeOverheadComment) { + core.debug('Found existing node overhead comment, updating it instead of creating a new one...'); + } + + const shouldComment = isNaN(thresholdNumber) || hasChanges(base, current, thresholdNumber) || nodeOverheadComment; + + if (shouldComment) { + const bodyParts = [ + NODE_OVERHEAD_HEADING, + 'Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.', + ]; + + if (baseIsNotLatest) { + bodyParts.push( + '⚠️ **Warning:** Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.', + ); + } + try { + bodyParts.push(markdownTable(formatResults(base, current))); + } catch (error) { + core.error('Error generating markdown table'); + throw error; + } + + if (baseWorkflowRun) { + bodyParts.push(''); + bodyParts.push(`[View base workflow run](${baseWorkflowRun.html_url})`); + } + + const body = bodyParts.join('\r\n'); + + try { + if (!nodeOverheadComment) { + await octokit.rest.issues.createComment({ + ...repo, + issue_number: pr.number, + body, + }); + } else { + await octokit.rest.issues.updateComment({ + ...repo, + comment_id: nodeOverheadComment.id, + body, + }); + } + } catch (error) { + core.error( + "Error updating comment. This can happen for PR's originating from a fork without write permissions.", + ); + } + } else { + core.debug('Skipping comment because there are no changes.'); + } + } catch (error) { + core.error(error); + setFailed(error.message); + } +} + +async function runNodeOverheadOnComparisonBranch() { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const resultsFilePath = getResultsFilePath(); + + const artifactClient = new DefaultArtifactClient(); + + const result = await getAveragedOverheadMeasurements(); + + try { + await fs.writeFile(resultsFilePath, JSON.stringify(result), 'utf8'); + } catch (error) { + core.error('Error parsing node overhead output. The output should be a json.'); + throw error; + } + + const globber = await glob.create(resultsFilePath, { + followSymbolicLinks: false, + }); + const files = await globber.glob(); + + await artifactClient.uploadArtifact(ARTIFACT_NAME, files, __dirname); +} + +run(); + +/** + * Use GitHub API to fetch artifact download url, then + * download and extract artifact to `downloadPath` + */ +async function downloadOtherWorkflowArtifact(octokit, { owner, repo, artifactId, artifactName, downloadPath }) { + const artifact = await octokit.rest.actions.downloadArtifact({ + owner, + repo, + artifact_id: artifactId, + archive_format: 'zip', + }); + + // Make sure output path exists + try { + await io.mkdirP(downloadPath); + } catch { + // ignore errors + } + + const downloadFile = path.resolve(downloadPath, `${artifactName}.zip`); + + await exec('wget', [ + '-nv', + '--retry-connrefused', + '--waitretry=1', + '--read-timeout=20', + '--timeout=15', + '-t', + '0', + '-O', + downloadFile, + artifact.url, + ]); + + await exec('unzip', ['-q', '-d', downloadPath, downloadFile], { + silent: true, + }); +} diff --git a/dev-packages/node-overhead-gh-action/lib/getArtifactsForBranchAndWorkflow.mjs b/dev-packages/node-overhead-gh-action/lib/getArtifactsForBranchAndWorkflow.mjs new file mode 100644 index 000000000000..ca7e4e20e9e5 --- /dev/null +++ b/dev-packages/node-overhead-gh-action/lib/getArtifactsForBranchAndWorkflow.mjs @@ -0,0 +1,122 @@ +import * as core from '@actions/core'; + +// max pages of workflows to pagination through +const DEFAULT_MAX_PAGES = 50; +// max results per page +const DEFAULT_PAGE_LIMIT = 10; + +/** + * Fetch artifacts from a workflow run from a branch + * + * This is a bit hacky since GitHub Actions currently does not directly + * support downloading artifacts from other workflows + */ +export async function getArtifactsForBranchAndWorkflow(octokit, { owner, repo, workflowName, branch, artifactName }) { + let repositoryWorkflow = null; + + // For debugging + const allWorkflows = []; + + // + // Find workflow id from `workflowName` + // + for await (const response of octokit.paginate.iterator(octokit.rest.actions.listRepoWorkflows, { + owner, + repo, + })) { + const targetWorkflow = response.data.find(({ name }) => name === workflowName); + + allWorkflows.push(...response.data.map(({ name }) => name)); + + // If not found in responses, continue to search on next page + if (!targetWorkflow) { + continue; + } + + repositoryWorkflow = targetWorkflow; + break; + } + + if (!repositoryWorkflow) { + core.info( + `Unable to find workflow with name "${workflowName}" in the repository. Found workflows: ${allWorkflows.join( + ', ', + )}`, + ); + return null; + } + + const workflow_id = repositoryWorkflow.id; + + let currentPage = 0; + let latestWorkflowRun = null; + + for await (const response of octokit.paginate.iterator(octokit.rest.actions.listWorkflowRuns, { + owner, + repo, + workflow_id, + branch, + per_page: DEFAULT_PAGE_LIMIT, + event: 'push', + })) { + if (!response.data.length) { + core.warning(`Workflow ${workflow_id} not found in branch ${branch}`); + return null; + } + + // Do not allow downloading artifacts from a fork. + const filtered = response.data.filter(workflowRun => workflowRun.head_repository.full_name === `${owner}/${repo}`); + + // Sort to ensure the latest workflow run is the first + filtered.sort((a, b) => { + return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); + }); + + // Store the first workflow run, to determine if this is the latest one... + if (!latestWorkflowRun) { + latestWorkflowRun = filtered[0]; + } + + // Search through workflow artifacts until we find a workflow run w/ artifact name that we are looking for + for (const workflowRun of filtered) { + core.info(`Checking artifacts for workflow run: ${workflowRun.html_url}`); + + const { + data: { artifacts }, + } = await octokit.rest.actions.listWorkflowRunArtifacts({ + owner, + repo, + run_id: workflowRun.id, + }); + + if (!artifacts) { + core.warning( + `Unable to fetch artifacts for branch: ${branch}, workflow: ${workflow_id}, workflowRunId: ${workflowRun.id}`, + ); + } else { + const foundArtifact = artifacts.find(({ name }) => name === artifactName); + if (foundArtifact) { + core.info(`Found suitable artifact: ${foundArtifact.url}`); + return { + artifact: foundArtifact, + workflowRun, + isLatest: latestWorkflowRun.id === workflowRun.id, + }; + } else { + core.info(`No artifact found for ${artifactName}, trying next workflow run...`); + } + } + } + + if (currentPage > DEFAULT_MAX_PAGES) { + core.warning(`Workflow ${workflow_id} not found in branch: ${branch}`); + return null; + } + + currentPage++; + } + + core.warning(`Artifact not found: ${artifactName}`); + core.endGroup(); + return null; +} diff --git a/dev-packages/node-overhead-gh-action/lib/getOverheadMeasurements.mjs b/dev-packages/node-overhead-gh-action/lib/getOverheadMeasurements.mjs new file mode 100644 index 000000000000..6cdd90cab34a --- /dev/null +++ b/dev-packages/node-overhead-gh-action/lib/getOverheadMeasurements.mjs @@ -0,0 +1,189 @@ +import { spawn } from 'child_process'; +import { dirname, join } from 'path'; +import treeKill from 'tree-kill'; +import { fileURLToPath } from 'url'; + +const DEBUG = !!process.env.DEBUG; + +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); + +async function getMeasurements(instrumentFile, autocannonCommand = 'yarn test:get') { + const args = [join(packageRoot, './src/app.mjs')]; + + if (instrumentFile) { + args.unshift('--import', join(packageRoot, instrumentFile)); + } + + const cmd = `node ${args.join(' ')}`; + + log('--------------------------------'); + log(`Getting measurements for "${cmd}"`); + + const killAppProcess = await startAppProcess(cmd); + + log('Example app listening, running autocannon...'); + + try { + const result = await startAutocannonProcess(autocannonCommand); + await killAppProcess(); + return result; + } catch (error) { + log(`Error running autocannon: ${error}`); + await killAppProcess(); + throw error; + } +} + +async function startAppProcess(cmd) { + const appProcess = spawn(cmd, { shell: true }); + + log('Child process started, waiting for example app...'); + + // Promise to keep track of the app process being closed + let resolveAppClose, rejectAppClose; + const appClosePromise = new Promise((resolve, reject) => { + resolveAppClose = resolve; + rejectAppClose = reject; + }); + + appProcess.on('close', code => { + if (code && code !== 0) { + rejectAppClose(new Error(`App process exited with code ${code}`)); + } else { + resolveAppClose(); + } + }); + + await new Promise((resolve, reject) => { + appProcess.stdout.on('data', data => { + log(`appProcess: ${data}`); + if (`${data}`.includes('Example app listening on port')) { + resolve(); + } + }); + + appProcess.stderr.on('data', data => { + log(`appProcess stderr: ${data}`); + killProcess(appProcess); + reject(data); + }); + }); + + return async () => { + log('Killing app process...'); + appProcess.stdin.end(); + appProcess.stdout.end(); + appProcess.stderr.end(); + + await killProcess(appProcess); + await appClosePromise; + log('App process killed'); + }; +} + +async function startAutocannonProcess(autocannonCommand) { + const autocannon = spawn(autocannonCommand, { + shell: true, + cwd: packageRoot, + }); + + let lastJson = undefined; + autocannon.stdout.on('data', data => { + log(`autocannon: ${data}`); + try { + lastJson = JSON.parse(data); + } catch { + // do nothing + } + }); + + return new Promise((resolve, reject) => { + autocannon.stderr.on('data', data => { + log(`autocannon stderr: ${data}`); + lastJson = undefined; + killProcess(autocannon); + }); + + autocannon.on('close', code => { + log(`autocannon closed with code ${code}`); + log(`Average requests: ${lastJson?.requests.average}`); + + if ((code && code !== 0) || !lastJson?.requests.average) { + reject(new Error(`Autocannon process exited with code ${code}`)); + } else { + resolve(Math.floor(lastJson.requests.average)); + } + }); + }); +} + +async function getOverheadMeasurements() { + const GET = { + baseline: await getMeasurements(undefined, 'yarn test:get'), + withInstrument: await getMeasurements('./src/instrument.mjs', 'yarn test:get'), + withInstrumentErrorOnly: await getMeasurements('./src/instrument-error-only.mjs', 'yarn test:get'), + }; + + const POST = { + baseline: await getMeasurements(undefined, 'yarn test:post'), + withInstrument: await getMeasurements('./src/instrument.mjs', 'yarn test:post'), + withInstrumentErrorOnly: await getMeasurements('./src/instrument-error-only.mjs', 'yarn test:post'), + }; + + return { + GET, + POST, + }; +} + +export async function getAveragedOverheadMeasurements() { + const results = []; + for (let i = 0; i < 2; i++) { + const result = await getOverheadMeasurements(); + results.push(result); + } + + // Calculate averages for each scenario + const averaged = { + GET: { + baseline: Math.floor(results.reduce((sum, r) => sum + r.GET.baseline, 0) / results.length), + withInstrument: Math.floor(results.reduce((sum, r) => sum + r.GET.withInstrument, 0) / results.length), + withInstrumentErrorOnly: Math.floor( + results.reduce((sum, r) => sum + r.GET.withInstrumentErrorOnly, 0) / results.length, + ), + }, + POST: { + baseline: Math.floor(results.reduce((sum, r) => sum + r.POST.baseline, 0) / results.length), + withInstrument: Math.floor(results.reduce((sum, r) => sum + r.POST.withInstrument, 0) / results.length), + withInstrumentErrorOnly: Math.floor( + results.reduce((sum, r) => sum + r.POST.withInstrumentErrorOnly, 0) / results.length, + ), + }, + }; + + return averaged; +} + +function log(message) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.log(message); + } +} + +function killProcess(process) { + return new Promise(resolve => { + const pid = process.pid; + + if (!pid) { + log('Process has no PID, fallback killing process...'); + process.kill(); + resolve(); + return; + } + + treeKill(pid, () => { + resolve(); + }); + }); +} diff --git a/dev-packages/node-overhead-gh-action/lib/markdown-table-formatter.mjs b/dev-packages/node-overhead-gh-action/lib/markdown-table-formatter.mjs new file mode 100644 index 000000000000..3119d6ad0edd --- /dev/null +++ b/dev-packages/node-overhead-gh-action/lib/markdown-table-formatter.mjs @@ -0,0 +1,112 @@ +const NODE_OVERHEAD_RESULTS_HEADER = ['Scenario', 'Requests/s', '% of Baseline', 'Prev. Requests/s', 'Change %']; + +const ROUND_NUMBER_FORMATTER = new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 0, +}); + +export function formatResults(baseScenarios, currentScenarios) { + const headers = NODE_OVERHEAD_RESULTS_HEADER; + + const scenarios = getScenarios(baseScenarios, currentScenarios); + const rows = [headers]; + + scenarios.forEach(scenario => { + const base = baseScenarios?.[scenario]; + const current = currentScenarios?.[scenario]; + const baseline = current?.baseline; + + rows.push(formatResult(`${scenario} Baseline`, base?.baseline, current?.baseline)); + rows.push(formatResult(`${scenario} With Sentry`, base?.withInstrument, current?.withInstrument, baseline)); + rows.push( + formatResult( + `${scenario} With Sentry (error only)`, + base?.withInstrumentErrorOnly, + current?.withInstrumentErrorOnly, + baseline, + ), + ); + }); + + return rows; +} +export function hasChanges(baseScenarios, currentScenarios, threshold = 0) { + if (!baseScenarios || !currentScenarios) { + return true; + } + + const names = ['baseline', 'withInstrument', 'withInstrumentErrorOnly']; + const scenarios = getScenarios(baseScenarios, currentScenarios); + + return scenarios.some(scenario => { + const base = baseScenarios?.[scenario]; + const current = currentScenarios?.[scenario]; + + return names.some(name => { + const baseResult = base[name]; + const currentResult = current[name]; + + if (!baseResult || !currentResult) { + return true; + } + + return Math.abs((currentResult - baseResult) / baseResult) * 100 > threshold; + }); + }); +} + +function formatResult(name, base, current, baseline) { + const currentValue = current ? ROUND_NUMBER_FORMATTER.format(current) : '-'; + const baseValue = base ? ROUND_NUMBER_FORMATTER.format(base) : '-'; + + return [ + name, + currentValue, + baseline != null ? formatPercentageDecrease(baseline, current) : '-', + baseValue, + formatPercentageChange(base, current), + ]; +} + +function formatPercentageChange(baseline, value) { + if (!baseline) { + return 'added'; + } + + if (!value) { + return 'removed'; + } + + const percentage = ((value - baseline) / baseline) * 100; + return formatChange(percentage); +} + +function formatPercentageDecrease(baseline, value) { + if (!baseline) { + return 'added'; + } + + if (!value) { + return 'removed'; + } + + const percentage = (value / baseline) * 100; + return `${ROUND_NUMBER_FORMATTER.format(percentage)}%`; +} + +function formatChange(value) { + if (value === 0) { + return '-'; + } + + if (value > 0) { + return `+${ROUND_NUMBER_FORMATTER.format(value)}%`; + } + + return `${ROUND_NUMBER_FORMATTER.format(value)}%`; +} + +function getScenarios(baseScenarios = {}, currentScenarios = {}) { + return Array.from(new Set([...Object.keys(baseScenarios), ...Object.keys(currentScenarios)])); +} diff --git a/dev-packages/node-overhead-gh-action/package.json b/dev-packages/node-overhead-gh-action/package.json new file mode 100644 index 000000000000..cb458eecdd24 --- /dev/null +++ b/dev-packages/node-overhead-gh-action/package.json @@ -0,0 +1,40 @@ +{ + "name": "@sentry-internal/node-overhead-gh-action", + "version": "10.5.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "type": "module", + "main": "index.mjs", + "scripts": { + "dev": "node ./run-local.mjs", + "start": "node ./src/app.mjs", + "start:sentry": "node --import ./src/instrument.mjs ./src/app.mjs", + "start:sentry-error-only": "node --import ./src/instrument-error-only.mjs ./src/app.mjs", + "test:get": "autocannon --json -c 100 -p 10 -d 10 -W [ -c 100 -d 5] http://localhost:3030/test-get", + "test:post": "autocannon --json -m POST -b \"{\\\"data\\\":\\\"test\\\"}\" --headers \"Content-type: application/json\" -c 100 -p 10 -d 10 -W [ -c 100 -d 5] http://localhost:3030/test-post", + "clean": "rimraf -g **/node_modules", + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix" + }, + "dependencies": { + "@sentry/node": "10.8.0", + "express": "^4.21.1" + }, + "devDependencies": { + "autocannon": "^8.0.0", + "@actions/artifact": "2.1.11", + "@actions/core": "1.10.1", + "@actions/exec": "1.1.1", + "@actions/github": "^5.0.0", + "@actions/glob": "0.4.0", + "@actions/io": "1.1.3", + "markdown-table": "3.0.3", + "tree-kill": "1.2.2" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/node-overhead-gh-action/run-local.mjs b/dev-packages/node-overhead-gh-action/run-local.mjs new file mode 100644 index 000000000000..fd890d559b5b --- /dev/null +++ b/dev-packages/node-overhead-gh-action/run-local.mjs @@ -0,0 +1,11 @@ +import { getAveragedOverheadMeasurements } from './lib/getOverheadMeasurements.mjs'; +import { formatResults } from './lib/markdown-table-formatter.mjs'; + +async function run() { + const measurements = await getAveragedOverheadMeasurements(); + + // eslint-disable-next-line no-console + console.log(formatResults(undefined, measurements)); +} + +run(); diff --git a/dev-packages/node-overhead-gh-action/src/app.mjs b/dev-packages/node-overhead-gh-action/src/app.mjs new file mode 100644 index 000000000000..370f2e7f7c8c --- /dev/null +++ b/dev-packages/node-overhead-gh-action/src/app.mjs @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.use(express.json()); + +app.get('/test-get', function (req, res) { + res.send({ version: 'v1' }); +}); + +app.post('/test-post', function (req, res) { + const body = req.body; + res.send(generateResponse(body)); +}); + +Sentry.setupExpressErrorHandler(app); + +app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`Example app listening on port ${port}`); +}); + +// This is complicated on purpose to simulate a real-world response +function generateResponse(body) { + const bodyStr = JSON.stringify(body); + const RES_BODY_SIZE = 1000; + + const bodyLen = bodyStr.length; + let resBody = ''; + for (let i = 0; i < RES_BODY_SIZE; i++) { + resBody += `${i}${bodyStr[i % bodyLen]}-`; + } + return { version: 'v1', length: bodyLen, resBody }; +} diff --git a/dev-packages/node-overhead-gh-action/src/instrument-error-only.mjs b/dev-packages/node-overhead-gh-action/src/instrument-error-only.mjs new file mode 100644 index 000000000000..6476a071226a --- /dev/null +++ b/dev-packages/node-overhead-gh-action/src/instrument-error-only.mjs @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN || 'https://1234567890@sentry.io/1234567890', +}); diff --git a/dev-packages/node-overhead-gh-action/src/instrument.mjs b/dev-packages/node-overhead-gh-action/src/instrument.mjs new file mode 100644 index 000000000000..8a49ebb67a7e --- /dev/null +++ b/dev-packages/node-overhead-gh-action/src/instrument.mjs @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN || 'https://1234567890@sentry.io/1234567890', + tracesSampleRate: 1, +}); diff --git a/dev-packages/size-limit-gh-action/index.mjs b/dev-packages/size-limit-gh-action/index.mjs index d727cbfa5b8b..3dac81a3f080 100644 --- a/dev-packages/size-limit-gh-action/index.mjs +++ b/dev-packages/size-limit-gh-action/index.mjs @@ -59,7 +59,7 @@ async function run() { const comparisonBranch = getInput('comparison_branch'); const githubToken = getInput('github_token'); - const threshold = getInput('threshold'); + const threshold = getInput('threshold') || 0.05; if (comparisonBranch && !pr) { throw new Error('No PR found. Only pull_request workflows are supported.'); diff --git a/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs b/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs index 6d512b46afe1..ca7e4e20e9e5 100644 --- a/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs +++ b/dev-packages/size-limit-gh-action/utils/getArtifactsForBranchAndWorkflow.mjs @@ -5,12 +5,6 @@ const DEFAULT_MAX_PAGES = 50; // max results per page const DEFAULT_PAGE_LIMIT = 10; -/** - * Fetch artifacts from a workflow run from a branch - * - * This is a bit hacky since GitHub Actions currently does not directly - * support downloading artifacts from other workflows - */ /** * Fetch artifacts from a workflow run from a branch * diff --git a/package.json b/package.json index e22e139d0c2e..1b09c85451ed 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,8 @@ "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", "dev-packages/external-contributor-gh-action", - "dev-packages/rollup-utils" + "dev-packages/rollup-utils", + "dev-packages/node-overhead-gh-action" ], "devDependencies": { "@rollup/plugin-commonjs": "^25.0.7", diff --git a/yarn.lock b/yarn.lock index b89eda6bc0e5..a22ef7403e6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -416,6 +416,11 @@ resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== +"@assemblyscript/loader@^0.19.21": + version "0.19.23" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72" + integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw== + "@astrojs/compiler@^2.3.0", "@astrojs/compiler@^2.9.1": version "2.12.2" resolved "https://registry.yarnpkg.com/@astrojs/compiler/-/compiler-2.12.2.tgz#5913b6ec7efffebdfb37fae9a50122802ae08c64" @@ -2707,6 +2712,11 @@ exec-sh "^0.3.2" minimist "^1.2.0" +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -4902,6 +4912,13 @@ semver "^7.5.3" tar "^7.4.0" +"@minimistjs/subarg@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@minimistjs/subarg/-/subarg-1.0.0.tgz#484fdfebda9dc32087d7c7999ec6350684fb42d2" + integrity sha512-Q/ONBiM2zNeYUy0mVSO44mWWKYM3UHuEK43PKIOzJCbvUnPoMH1K+gk3cf1kgnCVJFlWmddahQQCmrmBGlk9jQ== + dependencies: + minimist "^1.1.0" + "@mjackson/node-fetch-server@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz#577c0c25d8aae9f69a97738b7b0d03d1471cdc49" @@ -10848,6 +10865,35 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +autocannon@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-8.0.0.tgz#72b3ade6ec63dca0dc3be157c873d0a27e3f3745" + integrity sha512-fMMcWc2JPFcUaqHeR6+PbmEpTxCrPZyBUM95oG4w3ngJ8NfBNas/ZXA+pTHXLqJ0UlFVTcy05GC25WxKx/M20A== + dependencies: + "@minimistjs/subarg" "^1.0.0" + chalk "^4.1.0" + char-spinner "^1.0.1" + cli-table3 "^0.6.0" + color-support "^1.1.1" + cross-argv "^2.0.0" + form-data "^4.0.0" + has-async-hooks "^1.0.0" + hdr-histogram-js "^3.0.0" + hdr-histogram-percentiles-obj "^3.0.0" + http-parser-js "^0.5.2" + hyperid "^3.0.0" + lodash.chunk "^4.2.0" + lodash.clonedeep "^4.5.0" + lodash.flatten "^4.4.0" + manage-path "^2.0.0" + on-net-listen "^1.1.1" + pretty-bytes "^5.4.1" + progress "^2.0.3" + reinterval "^1.1.0" + retimer "^3.0.0" + semver "^7.3.2" + timestring "^6.0.0" + autoprefixer@^10.4.13, autoprefixer@^10.4.19, autoprefixer@^10.4.20, autoprefixer@^10.4.8: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -11988,7 +12034,7 @@ buffer-more-ints@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== -buffer@^5.5.0: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -12405,6 +12451,11 @@ chalk@^5.0.0, chalk@^5.2.0, chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +char-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081" + integrity sha512-acv43vqJ0+N0rD+Uw3pDHSxP30FHrywu2NO6/wBaHChJIizpDeBUd6NjqhNhy9LGaEAhZAXn46QzmlAvIWd16g== + character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" @@ -12603,6 +12654,15 @@ cli-spinners@^2.0.0, cli-spinners@^2.5.0, cli-spinners@^2.9.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35" integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ== +cli-table3@^0.6.0: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + cli-table@^0.3.1: version "0.3.6" resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.6.tgz#e9d6aa859c7fe636981fd3787378c2a20bce92fc" @@ -12744,7 +12804,7 @@ color-string@^1.6.0, color-string@^1.9.0: color-name "^1.0.0" simple-swizzle "^0.2.2" -color-support@^1.1.3: +color-support@^1.1.1, color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== @@ -13381,6 +13441,11 @@ cronstrue@^2.50.0: resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.50.0.tgz#eabba0f915f186765258b707b7a3950c663b5573" integrity sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg== +cross-argv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cross-argv/-/cross-argv-2.0.0.tgz#2e7907ba3246f82c967623a3e8525925bbd6c0ad" + integrity sha512-YIaY9TR5Nxeb8SMdtrU8asWVM4jqJDNDYlKV21LxtYcfNJhp1kEsgSa6qXwXgzN0WQWGODps0+TlGp2xQSHwOg== + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -18026,6 +18091,11 @@ has-ansi@^3.0.0: dependencies: ansi-regex "^3.0.0" +has-async-hooks@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-async-hooks/-/has-async-hooks-1.0.0.tgz#3df965ade8cd2d9dbfdacfbca3e0a5152baaf204" + integrity sha512-YF0VPGjkxr7AyyQQNykX8zK4PvtEDsUJAPqwu06UFz1lb6EvI53sPh5H1kWxg8NXI5LsfRCZ8uX9NkYDZBb/mw== + has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -18312,6 +18382,15 @@ hdr-histogram-js@^2.0.1: base64-js "^1.2.0" pako "^1.0.3" +hdr-histogram-js@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-3.0.1.tgz#b281e90d6ca80ee656bc378dafa39d7239b90855" + integrity sha512-l3GSdZL1Jr1C0kyb461tUjEdrRPZr8Qry7jByltf5JGrA0xvqOSrxRBfcrJqqV/AMEtqqhHhC6w8HW0gn76tRQ== + dependencies: + "@assemblyscript/loader" "^0.19.21" + base64-js "^1.2.0" + pako "^1.0.3" + hdr-histogram-percentiles-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" @@ -18584,10 +18663,10 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" -http-parser-js@>=0.5.1: - version "0.5.3" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" - integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== +http-parser-js@>=0.5.1, http-parser-js@^0.5.2: + version "0.5.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.10.tgz#b3277bd6d7ed5588e20ea73bf724fcbe44609075" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== http-proxy-agent@^4.0.0: version "4.0.1" @@ -18703,6 +18782,15 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +hyperid@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hyperid/-/hyperid-3.3.0.tgz#2042bb296b7f1d5ba0797a5705469af0899c8556" + integrity sha512-7qhCVT4MJIoEsNcbhglhdmBKb09QtcmJNiIQGq7js/Khf5FtQQ9bzcAuloeqBeee7XD7JqDeve9KNlQya5tSGQ== + dependencies: + buffer "^5.2.1" + uuid "^8.3.2" + uuid-parse "^1.1.0" + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -20716,6 +20804,11 @@ lodash.camelcase@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= +lodash.chunk@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -20751,6 +20844,11 @@ lodash.flatten@^3.0.2: lodash._baseflatten "^3.0.0" lodash._isiterateecall "^3.0.0" +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== + lodash.foreach@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" @@ -21197,6 +21295,11 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +manage-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/manage-path/-/manage-path-2.0.0.tgz#f4cf8457b926eeee2a83b173501414bc76eb9597" + integrity sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A== + map-age-cleaner@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" @@ -22066,7 +22169,7 @@ minimist@^0.2.1: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.4.tgz#0085d5501e29033748a2f2a4da0180142697a475" integrity sha512-Pkrrm8NjyQ8yVt8Am9M+yUt74zE3iokhzbG1bFVNjLB92vwM71hf40RkEsryg98BujhVOncKm/C1xROxZ030LQ== -minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -23657,6 +23760,11 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-net-listen@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/on-net-listen/-/on-net-listen-1.1.2.tgz#671e55a81c910fa7e5b1e4d506545e9ea0f2e11c" + integrity sha512-y1HRYy8s/RlcBvDUwKXSmkODMdx4KSuIvloCnQYJ2LdBBC1asY4HtfhXwe3UWknLakATZDnbzht2Ijw3M1EqFg== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -25405,7 +25513,7 @@ prettier@^3.0.0, prettier@^3.6.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== -pretty-bytes@^5.3.0: +pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== @@ -26392,6 +26500,11 @@ rehype@^12.0.1: rehype-stringify "^9.0.0" unified "^10.0.0" +reinterval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reinterval/-/reinterval-1.1.0.tgz#3361ecfa3ca6c18283380dd0bb9546f390f5ece7" + integrity sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ== + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" @@ -26739,6 +26852,11 @@ retext@^8.1.0: retext-stringify "^3.0.0" unified "^10.0.0" +retimer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df" + integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA== + retry-request@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.3.tgz#d5f74daf261372cff58d08b0a1979b4d7cab0fde" @@ -29072,6 +29190,11 @@ tildify@2.0.0: resolved "https://registry.yarnpkg.com/tildify/-/tildify-2.0.0.tgz#f205f3674d677ce698b7067a99e949ce03b4754a" integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== +timestring@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" + integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== + tiny-glob@0.2.9, tiny-glob@^0.2.9: version "0.2.9" resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" @@ -30354,6 +30477,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid-parse@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" + integrity sha512-OdmXxA8rDsQ7YpNVbKSJkNzTw2I+S5WsbMDnCtIWSQaosNAcWtFuI/YK1TjzUI6nbkgiqEyh8gWngfcv8Asd9A== + uuid-v4@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/uuid-v4/-/uuid-v4-0.1.0.tgz#62d7b310406f6cecfea1528c69f1e8e0bcec5a3a"