diff --git a/.github/actions/install-pnpm/action.yml b/.github/actions/install-pnpm/action.yml index d2d28a09dbb4..a23f372a8f9b 100644 --- a/.github/actions/install-pnpm/action.yml +++ b/.github/actions/install-pnpm/action.yml @@ -13,7 +13,7 @@ runs: shell: bash env: PACKAGE_JSON: ${{ inputs.package-json }} - # Keep in sync with `getPnpmMajor` in `dev-packages/e2e-tests/lib/pnpmOverrides.ts`. It writes the overrides in + # Keep in sync with `getPnpmMajor` in `dev-packages/e2e-tests/lib/pnpmOverrides.mjs`. It writes the overrides in # the format of the version it finds, so a different lookup breaks the install. run: | package_json="$PACKAGE_JSON" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aecb25032c9a..81d5b1882f17 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1095,11 +1095,6 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Restore tarball artifacts uses: actions/download-artifact@v8 with: @@ -1119,6 +1114,13 @@ jobs: name: build-layer-output path: ${{ env.BUILD_LAYER_PATH }} + # The Lambda functions install the built workspace packages directly (see `src/stack.ts`). + - name: Restore build artifacts + uses: actions/download-artifact@v8 + if: matrix.test-application == 'aws-serverless' + with: + name: build-output + # Installs Playwright while the app builds. # Started after the artifact downloads: starting next to them made `download-artifact` fail to read the event file. - name: Install Playwright @@ -1175,7 +1177,7 @@ jobs: - name: Pre-process E2E Test Dumps if: failure() run: | - node ./scripts/normalize-e2e-test-dump-transaction-events.js + node ./scripts/normalize-e2e-test-dump-transaction-events.js ${{ runner.temp }}/test-application/event-dumps - name: Upload E2E Test Event Dumps uses: actions/upload-artifact@v7 @@ -1248,11 +1250,6 @@ jobs: uses: denoland/setup-deno@v2.0.5 with: deno-version: ${{ matrix.deno-version || 'v2.8.3' }} - - name: Restore caches - uses: ./.github/actions/restore-cache - with: - dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Restore tarball artifacts uses: actions/download-artifact@v8 with: @@ -1308,7 +1305,7 @@ jobs: - name: Pre-process E2E Test Dumps if: failure() run: | - node ./scripts/normalize-e2e-test-dump-transaction-events.js + node ./scripts/normalize-e2e-test-dump-transaction-events.js ${{ runner.temp }}/test-application/event-dumps - name: Upload E2E Test Event Dumps uses: actions/upload-artifact@v7 diff --git a/dev-packages/e2e-tests/ciCopyToTemp.mjs b/dev-packages/e2e-tests/ciCopyToTemp.mjs new file mode 100644 index 000000000000..f257e00d12a7 --- /dev/null +++ b/dev-packages/e2e-tests/ciCopyToTemp.mjs @@ -0,0 +1,20 @@ +// @ts-check +/* eslint-disable no-console */ + +import { copyToTemp } from './lib/copyToTemp.mjs'; +import { applyRuntimeFiles } from './lib/runtimeFiles.mjs'; + +const originalPath = process.argv[2]; +const tmpDirPath = process.argv[3]; + +if (!originalPath || !tmpDirPath) { + throw new Error('Original path and tmp dir path are required'); +} + +console.log(`Copying ${originalPath} to ${tmpDirPath}...`); + +await copyToTemp(originalPath, tmpDirPath); + +if (process.env.RUNTIME) { + applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); +} diff --git a/dev-packages/e2e-tests/ciCopyToTemp.ts b/dev-packages/e2e-tests/ciCopyToTemp.ts deleted file mode 100644 index 649fe01fed3f..000000000000 --- a/dev-packages/e2e-tests/ciCopyToTemp.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable no-console */ - -import { copyToTemp } from './lib/copyToTemp'; -import { applyRuntimeFiles } from './lib/runtimeFiles'; - -async function run(): Promise { - const originalPath = process.argv[2]; - const tmpDirPath = process.argv[3]; - - if (!originalPath || !tmpDirPath) { - throw new Error('Original path and tmp dir path are required'); - } - - console.log(`Copying ${originalPath} to ${tmpDirPath}...`); - - await copyToTemp(originalPath, tmpDirPath); - - if (process.env.RUNTIME) { - applyRuntimeFiles(tmpDirPath, process.env.RUNTIME); - } -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/ciPnpmOverrides.mjs b/dev-packages/e2e-tests/ciPnpmOverrides.mjs new file mode 100644 index 000000000000..a3d6d643c6fa --- /dev/null +++ b/dev-packages/e2e-tests/ciPnpmOverrides.mjs @@ -0,0 +1,12 @@ +// @ts-check +import * as path from 'node:path'; +import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; + +const tmpDirPath = process.argv[2]; +const packedDirPath = process.argv[3]; + +if (!tmpDirPath || !packedDirPath) { + throw new Error('Tmp dir path and packed dir path are required'); +} + +await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath)); diff --git a/dev-packages/e2e-tests/ciPnpmOverrides.ts b/dev-packages/e2e-tests/ciPnpmOverrides.ts deleted file mode 100644 index 909a59fa2e9d..000000000000 --- a/dev-packages/e2e-tests/ciPnpmOverrides.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* eslint-disable no-console */ - -import { addPnpmOverrides } from './lib/pnpmOverrides'; -import * as path from 'path'; - -async function run(): Promise { - const tmpDirPath = process.argv[2]; - const packedDirPath = process.argv[3]; - - if (!tmpDirPath || !packedDirPath) { - throw new Error('Tmp dir path and packed dir path are required'); - } - - await addPnpmOverrides(path.resolve(tmpDirPath), path.resolve(packedDirPath)); -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/lib/copyToTemp.ts b/dev-packages/e2e-tests/lib/copyToTemp.mjs similarity index 69% rename from dev-packages/e2e-tests/lib/copyToTemp.ts rename to dev-packages/e2e-tests/lib/copyToTemp.mjs index c45a5793e2fc..ae7f5e04d5fe 100644 --- a/dev-packages/e2e-tests/lib/copyToTemp.ts +++ b/dev-packages/e2e-tests/lib/copyToTemp.mjs @@ -1,22 +1,31 @@ +// @ts-check /* eslint-disable no-console */ -import { readFileSync, writeFileSync } from 'fs'; -import { cp } from 'fs/promises'; -import { isAbsolute, join, resolve } from 'path'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { cp } from 'node:fs/promises'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; -export async function copyToTemp(originalPath: string, tmpDirPath: string): Promise { +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * @param {string} originalPath + * @param {string} tmpDirPath + * @returns {Promise} + */ +export async function copyToTemp(originalPath, tmpDirPath) { // copy files to tmp dir await cp(originalPath, tmpDirPath, { recursive: true }); fixPackageJson(tmpDirPath); } -function fixPackageJson(cwd: string): void { +/** + * @param {string} cwd + */ +function fixPackageJson(cwd) { const packageJsonPath = join(cwd, 'package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { - dependencies?: Record; - devDependencies?: Record; - volta?: Record; - }; + /** @type {{ dependencies?: Record; devDependencies?: Record; volta?: Record }} */ + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); // 1. Fix file dependencies if (packageJson.dependencies) { @@ -45,8 +54,11 @@ function fixPackageJson(cwd: string): void { writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } -// Exported for pnpmOverrides as well -export function fixFileLinkDependencies(dependencyObj: Record): void { +/** + * Exported for pnpmOverrides as well + * @param {Record} dependencyObj + */ +export function fixFileLinkDependencies(dependencyObj) { for (const [key, value] of Object.entries(dependencyObj)) { const prefix = value.startsWith('link:') ? 'link:' : value.startsWith('file:') ? 'file:' : null; if (!prefix) { diff --git a/dev-packages/e2e-tests/lib/packedTarballUtils.mjs b/dev-packages/e2e-tests/lib/packedTarballUtils.mjs new file mode 100644 index 000000000000..c46a94ff9578 --- /dev/null +++ b/dev-packages/e2e-tests/lib/packedTarballUtils.mjs @@ -0,0 +1,83 @@ +// @ts-check +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const E2E_TESTS_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..'); + +// test-utils is not published, but test apps install it from its tarball too: with a `link:`, its dependencies would +// resolve from the monorepo's root `node_modules` instead of the app's. +const TARBALL_PACKAGE_DIRS = [ + ...fs.readdirSync(path.join(REPOSITORY_ROOT, 'packages')).map(dir => path.join(REPOSITORY_ROOT, 'packages', dir)), + path.join(REPOSITORY_ROOT, 'dev-packages', 'test-utils'), +]; + +/** + * Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version. + * @returns {{ name: string; tarballPath: string }[]} The package names and their tarball paths, sorted by name. + */ +export function getPackedTarballs() { + const version = getE2eTestsPackageVersion(); + const tarballs = []; + + for (const packageDir of TARBALL_PACKAGE_DIRS) { + const packageJsonPath = path.join(packageDir, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + continue; + } + + const name = readJson(packageJsonPath).name; + if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { + continue; + } + + const tarballPath = path.join(packageDir, versionedTarballFilename(name, version)); + if (fs.existsSync(tarballPath)) { + tarballs.push({ name, tarballPath }); + } + } + + return tarballs.sort((a, b) => a.name.localeCompare(b.name)); +} + +/** + * Stable symlink name in `packed/` (no version segment). + * @param {string} packageName + * @returns {string} + */ +export function packedSymlinkFilename(packageName) { + return `${npmPackBasename(packageName)}-packed.tgz`; +} + +/** + * Versioned tarball filename produced by `npm pack` in a package directory. + * @param {string} packageName + * @param {string} version + * @returns {string} + */ +function versionedTarballFilename(packageName, version) { + return `${npmPackBasename(packageName)}-${version}.tgz`; +} + +/** + * npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`. + * @param {string} packageName + * @returns {string} + */ +function npmPackBasename(packageName) { + return packageName.replace(/^@/, '').replace(/\//g, '-'); +} + +/** + * @param {string} filePath + * @returns {any} + */ +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +/** @returns {string} */ +export function getE2eTestsPackageVersion() { + return readJson(path.join(E2E_TESTS_ROOT, 'package.json')).version; +} diff --git a/dev-packages/e2e-tests/lib/packedTarballUtils.ts b/dev-packages/e2e-tests/lib/packedTarballUtils.ts deleted file mode 100644 index d7bbeefcb8e7..000000000000 --- a/dev-packages/e2e-tests/lib/packedTarballUtils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import { sync as globSync } from 'glob'; - -const E2E_TESTS_ROOT = path.resolve(__dirname, '..'); -const REPOSITORY_ROOT = path.resolve(E2E_TESTS_ROOT, '../..'); - -// test-utils is not published, but test apps install it from its tarball too: with a `link:`, its dependencies would -// resolve from the monorepo's root `node_modules` instead of the app's. -export const TARBALL_PACKAGE_JSON_GLOB = '{packages/*,dev-packages/test-utils}/package.json'; - -/** - * Workspace @sentry and @sentry-internal packages that have a built tarball for the E2E version. - * @returns The names of the Sentry tarball packages. - */ -export function getPublishedSentryTarballPackageNames(): string[] { - const version = getE2eTestsPackageVersion(); - const names: string[] = []; - - for (const packageJsonPath of globSync(TARBALL_PACKAGE_JSON_GLOB, { - cwd: REPOSITORY_ROOT, - absolute: true, - })) { - const pkg = readJson<{ name?: string }>(packageJsonPath); - const name = pkg.name; - if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { - continue; - } - const packageDir = path.dirname(packageJsonPath); - const tarball = path.join(packageDir, versionedTarballFilename(name, version)); - if (fs.existsSync(tarball)) { - names.push(name); - } - } - - return names.sort(); -} - -/** Stable symlink name in `packed/` (no version segment). */ -export function packedSymlinkFilename(packageName: string): string { - return `${npmPackBasename(packageName)}-packed.tgz`; -} - -/** - * Versioned tarball filename produced by `npm pack` in a package directory. - */ -export function versionedTarballFilename(packageName: string, version: string): string { - return `${npmPackBasename(packageName)}-${version}.tgz`; -} - -/** - * npm pack tarball basename (without version and .tgz), e.g. `@sentry/core` -> `sentry-core`. - */ -function npmPackBasename(packageName: string): string { - return packageName.replace(/^@/, '').replace(/\//g, '-'); -} - -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -} - -function getE2eTestsPackageVersion(): string { - return readJson<{ version: string }>(path.join(E2E_TESTS_ROOT, 'package.json')).version; -} diff --git a/dev-packages/e2e-tests/lib/pnpmOverrides.ts b/dev-packages/e2e-tests/lib/pnpmOverrides.mjs similarity index 70% rename from dev-packages/e2e-tests/lib/pnpmOverrides.ts rename to dev-packages/e2e-tests/lib/pnpmOverrides.mjs index e9f79d05f71e..680d7b9240fb 100644 --- a/dev-packages/e2e-tests/lib/pnpmOverrides.ts +++ b/dev-packages/e2e-tests/lib/pnpmOverrides.mjs @@ -1,7 +1,10 @@ -import { readFile, writeFile } from 'fs/promises'; -import * as path from 'path'; -import { getPublishedSentryTarballPackageNames, packedSymlinkFilename } from './packedTarballUtils'; -import { fixFileLinkDependencies } from './copyToTemp'; +// @ts-check +import { readFile, writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import { fixFileLinkDependencies } from './copyToTemp.mjs'; +import { getPackedTarballs, packedSymlinkFilename } from './packedTarballUtils.mjs'; + +/** @typedef {{ pnpm?: { overrides?: Record }; volta?: { extends?: string; pnpm?: string } }} PackageJson */ /** * For a given temp test application directory, add pnpm overrides to pin internal Sentry dependencies to their packed tarballs. @@ -11,18 +14,16 @@ import { fixFileLinkDependencies } from './copyToTemp'; * package.json. A fixture's existing overrides take precedence in both cases. * https://pnpm.io/settings/dependency-resolution#overrides * - * @param tmpDirPath - The temporary directory path of the test application. - * @param packedDirPath - The path to the packed tarballs. - * @param packageNames - The names of the internal Sentry packages to pin to the packed tarballs. + * @param {string} tmpDirPath - The temporary directory path of the test application. + * @param {string} packedDirPath - The path to the packed tarballs. + * @returns {Promise} */ -export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string): Promise { +export async function addPnpmOverrides(tmpDirPath, packedDirPath) { const packageJsonPath = path.join(tmpDirPath, 'package.json'); - const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as { - pnpm?: { overrides?: Record }; - volta?: { extends?: string; pnpm?: string }; - }; + /** @type {PackageJson} */ + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')); - const packageNames = getPublishedSentryTarballPackageNames(); + const packageNames = getPackedTarballs().map(({ name }) => name); const overrides = Object.fromEntries( packageNames.map(packageName => [packageName, `file:${packedDirPath}/${packedSymlinkFilename(packageName)}`]), ); @@ -57,11 +58,13 @@ export async function addPnpmOverrides(tmpDirPath: string, packedDirPath: string console.log(`Added ${packageNames.length} internal Sentry package overrides`); } -// Keep in sync with `.github/actions/install-pnpm`, which installs the version this finds in CI. -async function getPnpmMajor( - packageJsonPath: string, - packageJson: { volta?: { extends?: string; pnpm?: string } }, -): Promise { +/** + * Keep in sync with `.github/actions/install-pnpm`, which installs the version this finds in CI. + * @param {string} packageJsonPath + * @param {PackageJson} packageJson + * @returns {Promise} + */ +async function getPnpmMajor(packageJsonPath, packageJson) { const pnpmVersion = packageJson.volta?.pnpm; if (pnpmVersion) { const major = /^(?\d+)(?:\.|$)/.exec(pnpmVersion)?.groups?.major; @@ -75,16 +78,19 @@ async function getPnpmMajor( const voltaExtends = packageJson.volta?.extends; if (voltaExtends) { const extendedPackageJsonPath = path.resolve(path.dirname(packageJsonPath), voltaExtends); - const extendedPackageJson = JSON.parse(await readFile(extendedPackageJsonPath, 'utf8')) as { - volta?: { extends?: string; pnpm?: string }; - }; + /** @type {PackageJson} */ + const extendedPackageJson = JSON.parse(await readFile(extendedPackageJsonPath, 'utf8')); return getPnpmMajor(extendedPackageJsonPath, extendedPackageJson); } throw new Error(`No Volta pnpm version found for ${packageJsonPath}`); } -function parseWorkspaceOverrides(workspace: string): Record { +/** + * @param {string} workspace + * @returns {Record} + */ +function parseWorkspaceOverrides(workspace) { const overrides = /^overrides:\n((?:[ \t]+.*(?:\n|$))*)/m.exec(workspace)?.[1]; if (!overrides) { return {}; @@ -100,16 +106,24 @@ function parseWorkspaceOverrides(workspace: string): Record { throw new Error(`Unsupported pnpm workspace override: ${line.trim()}`); } - return [JSON.parse(match[1]) as string, JSON.parse(match[2]) as string]; + return [JSON.parse(match[1]), JSON.parse(match[2])]; }), ); } -function removeWorkspaceOverrides(workspace: string): string { +/** + * @param {string} workspace + * @returns {string} + */ +function removeWorkspaceOverrides(workspace) { return workspace.replace(/^overrides:\n(?:[ \t]+.*(?:\n|$))*/m, ''); } -function formatWorkspaceOverrides(overrides: Record): string { +/** + * @param {Record} overrides + * @returns {string} + */ +function formatWorkspaceOverrides(overrides) { return Object.entries(overrides) .map(([packageName, target]) => ` ${JSON.stringify(packageName)}: ${JSON.stringify(target)}`) .join('\n'); diff --git a/dev-packages/e2e-tests/lib/runtimeFiles.ts b/dev-packages/e2e-tests/lib/runtimeFiles.mjs similarity index 81% rename from dev-packages/e2e-tests/lib/runtimeFiles.ts rename to dev-packages/e2e-tests/lib/runtimeFiles.mjs index a29c0bc890d3..505d7309847b 100644 --- a/dev-packages/e2e-tests/lib/runtimeFiles.ts +++ b/dev-packages/e2e-tests/lib/runtimeFiles.mjs @@ -1,12 +1,15 @@ +// @ts-check /* eslint-disable no-console */ -import { copyFileSync, existsSync, readdirSync } from 'fs'; -import { join } from 'path'; +import { copyFileSync, existsSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; /** * Copies each file in `dir` that has `runtime` as a part of its name over the existing file without that part, for * example `app/entry.server.cloudflare.tsx` over `app/entry.server.tsx`. + * @param {string} dir + * @param {string} runtime */ -export function applyRuntimeFiles(dir: string, runtime: string): void { +export function applyRuntimeFiles(dir, runtime) { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) { continue; diff --git a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs new file mode 100644 index 000000000000..f74ef9488f75 --- /dev/null +++ b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.mjs @@ -0,0 +1,38 @@ +// @ts-check +/* eslint-disable no-console */ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getE2eTestsPackageVersion, getPackedTarballs, packedSymlinkFilename } from './packedTarballUtils.mjs'; + +const e2eTestsRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repositoryRoot = path.resolve(e2eTestsRoot, '../..'); +const packedDir = path.join(e2eTestsRoot, 'packed'); + +/** + * Ensures `packed/-packed.tgz` symlinks point at the current versioned tarballs under `packages/*`. + * Run after `yarn build:tarball` at the repo root (or from CI after restoring the tarball cache). + */ +export function syncPackedTarballSymlinks() { + fs.mkdirSync(packedDir, { recursive: true }); + + for (const entry of fs.readdirSync(packedDir, { withFileTypes: true })) { + if (!entry.name.endsWith('-packed.tgz')) { + continue; + } + fs.rmSync(path.join(packedDir, entry.name), { recursive: true, force: true }); + } + + const tarballs = getPackedTarballs(); + if (tarballs.length === 0) { + throw new Error( + `No packed tarballs found for version ${getE2eTestsPackageVersion()} under packages/*/. Run "yarn build:tarball" at the repository root.`, + ); + } + + for (const { name, tarballPath } of tarballs) { + fs.symlinkSync(tarballPath, path.join(packedDir, packedSymlinkFilename(name))); + } + + console.log(`Linked ${tarballs.length} tarball symlinks in ${path.relative(repositoryRoot, packedDir) || 'packed'}.`); +} diff --git a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts b/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts deleted file mode 100644 index cdc42832a979..000000000000 --- a/dev-packages/e2e-tests/lib/syncPackedTarballSymlinks.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable no-console */ -import * as fs from 'fs'; -import * as path from 'path'; -import { sync as globSync } from 'glob'; -import { packedSymlinkFilename, TARBALL_PACKAGE_JSON_GLOB, versionedTarballFilename } from './packedTarballUtils'; - -const e2eTestsRoot = path.resolve(__dirname, '..'); -const repositoryRoot = path.resolve(e2eTestsRoot, '../..'); -const packedDir = path.join(e2eTestsRoot, 'packed'); - -function readJson(filePath: string): T { - return JSON.parse(fs.readFileSync(filePath, 'utf8')) as T; -} - -/** - * Ensures `packed/-packed.tgz` symlinks point at the current versioned tarballs. - * Run after `yarn build:tarball` at the repo root (or from CI after restoring the tarball cache). - */ -export function syncPackedTarballSymlinks(): void { - const { version } = readJson<{ version: string }>(path.join(e2eTestsRoot, 'package.json')); - - fs.mkdirSync(packedDir, { recursive: true }); - - for (const entry of fs.readdirSync(packedDir, { withFileTypes: true })) { - if (!entry.name.endsWith('-packed.tgz')) { - continue; - } - fs.rmSync(path.join(packedDir, entry.name), { recursive: true, force: true }); - } - - const packageJsonPaths = globSync(TARBALL_PACKAGE_JSON_GLOB, { - cwd: repositoryRoot, - absolute: true, - }); - - let linked = 0; - for (const packageJsonPath of packageJsonPaths) { - const pkg = readJson<{ name?: string }>(packageJsonPath); - const name = pkg.name; - if (!name || (!name.startsWith('@sentry/') && !name.startsWith('@sentry-internal/'))) { - continue; - } - - const packageDir = path.dirname(packageJsonPath); - const expectedTarball = path.join(packageDir, versionedTarballFilename(name, version)); - - if (!fs.existsSync(expectedTarball)) { - continue; - } - - const linkName = packedSymlinkFilename(name); - const linkPath = path.join(packedDir, linkName); - - fs.symlinkSync(expectedTarball, linkPath); - linked++; - } - - if (linked === 0) { - throw new Error( - `No packed tarballs found for version ${version}. Run "yarn build:tarball" at the repository root.`, - ); - } - - console.log(`Linked ${linked} tarball symlinks in ${path.relative(repositoryRoot, packedDir) || 'packed'}.`); -} diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index a92e96a4d64c..7abbb49fd143 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -6,27 +6,21 @@ "scripts": { "lint:fix": "oxlint . --fix --type-aware", "lint": "oxlint . --type-aware", - "lint:ts": "tsc --noEmit", "test:e2e": "run-s test:prepare test:validate test:run", - "test:run": "tsx run.ts", - "test:prepare": "tsx prepare.ts", - "test:validate": "tsx validate-packed-tarball-setup.ts", + "test:run": "node run.mjs", + "test:prepare": "node prepare.mjs", + "test:validate": "node validate-packed-tarball-setup.mjs", "clean": "rimraf tmp node_modules packed && yarn clean:test-applications && yarn clean:pnpm", "ci:build-matrix": "node ./lib/getTestMatrix.mjs", "ci:build-matrix-optional": "node ./lib/getTestMatrix.mjs --optional=true", - "ci:copy-to-temp": "tsx ./ciCopyToTemp.ts", - "ci:pnpm-overrides": "tsx ./ciPnpmOverrides.ts", + "ci:copy-to-temp": "node ./ciCopyToTemp.mjs", + "ci:pnpm-overrides": "node ./ciPnpmOverrides.mjs", "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,.astro,.output,pnpm-lock.yaml,.last-run.json,test-results,.angular,event-dumps}", "clean:pnpm": "pnpm store prune" }, "devDependencies": { - "@types/node": "^18.19.1", - "dotenv": "17.4.2", - "esbuild": "0.28.1", "eslint-plugin-regexp": "^3.1.0", - "glob": "^13.0.6", - "rimraf": "^6.1.3", - "tsx": "^4.23.0" + "rimraf": "^6.1.3" }, "volta": { "extends": "../../package.json" diff --git a/dev-packages/e2e-tests/prepare.mjs b/dev-packages/e2e-tests/prepare.mjs new file mode 100644 index 000000000000..bcaea94b2895 --- /dev/null +++ b/dev-packages/e2e-tests/prepare.mjs @@ -0,0 +1,4 @@ +// @ts-check +import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks.mjs'; + +syncPackedTarballSymlinks(); diff --git a/dev-packages/e2e-tests/prepare.ts b/dev-packages/e2e-tests/prepare.ts deleted file mode 100644 index 049b727b1b21..000000000000 --- a/dev-packages/e2e-tests/prepare.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable no-console */ -import * as dotenv from 'dotenv'; -import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; - -async function run(): Promise { - // Load environment variables from .env file locally - dotenv.config(); - - syncPackedTarballSymlinks(); -} - -run().catch(error => { - console.error(error); - process.exit(1); -}); diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.mjs similarity index 79% rename from dev-packages/e2e-tests/run.ts rename to dev-packages/e2e-tests/run.mjs index f3a7f115925e..677880d5537c 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.mjs @@ -1,43 +1,41 @@ +// @ts-check /* eslint-disable no-console */ -import { spawn } from 'child_process'; -import * as dotenv from 'dotenv'; -import { mkdir, mkdtemp, readFile, rm } from 'fs/promises'; -import { sync as globSync } from 'glob'; -import { tmpdir } from 'os'; -import { join, resolve } from 'path'; -import { copyToTemp } from './lib/copyToTemp'; -import { applyRuntimeFiles } from './lib/runtimeFiles'; -import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks'; -import { addPnpmOverrides } from './lib/pnpmOverrides'; - -interface SentryTestVariant { - 'build-command': string; - 'assert-command'?: string; - label?: string; - skip?: boolean; - /** Runtime of the variant (`bun`, `deno` or `cloudflare`). Sets `RUNTIME` and selects the runtime-specific files. */ - runtime?: string; -} - -interface PackageJson { - sentryTest?: { - variants?: SentryTestVariant[]; - optionalVariants?: SentryTestVariant[]; - }; -} +import { spawn } from 'node:child_process'; +import { existsSync, readdirSync } from 'node:fs'; +import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { copyToTemp } from './lib/copyToTemp.mjs'; +import { addPnpmOverrides } from './lib/pnpmOverrides.mjs'; +import { applyRuntimeFiles } from './lib/runtimeFiles.mjs'; +import { syncPackedTarballSymlinks } from './lib/syncPackedTarballSymlinks.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * `runtime` is the runtime of the variant (`bun`, `deno` or `cloudflare`). It sets `RUNTIME` and selects the + * runtime-specific files. + * @typedef {{ 'build-command': string; 'assert-command'?: string; label?: string; skip?: boolean; runtime?: string }} SentryTestVariant + */ + +/** @typedef {{ sentryTest?: { variants?: SentryTestVariant[]; optionalVariants?: SentryTestVariant[] } }} PackageJson */ const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; -function asyncExec( - command: string | string[], - options: { env: Record; cwd: string }, -): Promise { +/** + * @param {string | string[]} command + * @param {{ env: Record; cwd: string }} options + * @returns {Promise} + */ +function asyncExec(command, options) { return new Promise((resolve, reject) => { // If command is an array, use spawn with separate command and args (safer) // If command is a string, maintain backward compatibility with shell: true - let process: ReturnType; + /** @type {ReturnType} */ + let process; if (typeof command === 'string') { process = spawn(command, { ...options, shell: true }); } else { @@ -76,27 +74,28 @@ function asyncExec( }); } -function findMatchingVariant(variants: SentryTestVariant[], variantLabel: string): SentryTestVariant | undefined { +/** + * @param {SentryTestVariant[]} variants + * @param {string} variantLabel + * @returns {SentryTestVariant | undefined} + */ +function findMatchingVariant(variants, variantLabel) { const variantLabelLower = variantLabel.toLowerCase(); return variants.find(variant => variant.label?.toLowerCase().includes(variantLabelLower)); } -async function getVariantBuildCommand( - packageJsonPath: string, - variantLabel: string, - testAppPath: string, -): Promise<{ - buildCommand: string; - assertCommand: string; - testLabel: string; - matchedVariantLabel?: string; - skip?: boolean; - runtime?: string; -}> { +/** + * @param {string} packageJsonPath + * @param {string} variantLabel + * @param {string} testAppPath + * @returns {Promise<{ buildCommand: string; assertCommand: string; testLabel: string; matchedVariantLabel?: string; skip?: boolean; runtime?: string }>} + */ +async function getVariantBuildCommand(packageJsonPath, variantLabel, testAppPath) { try { const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); - const packageJson: PackageJson = JSON.parse(packageJsonContent); + /** @type {PackageJson} */ + const packageJson = JSON.parse(packageJsonContent); const allVariants = [ ...(packageJson.sentryTest?.variants || []), @@ -128,9 +127,12 @@ async function getVariantBuildCommand( }; } -async function run(): Promise { +async function run() { // Load environment variables from .env file locally - dotenv.config(); + const envFile = join(__dirname, '.env'); + if (existsSync(envFile)) { + process.loadEnvFile(envFile); + } // Allow to run a single app only via `yarn test:run ` const appName = process.argv[2] || ''; @@ -138,7 +140,8 @@ async function run(): Promise { const allTestFlags = process.argv.slice(3); // Check for --variant flag - let variantLabel: string | undefined; + /** @type {string | undefined} */ + let variantLabel; let skipNextFlag = false; const testFlags = allTestFlags.filter((flag, index) => { @@ -216,7 +219,11 @@ async function run(): Promise { await asyncExec('pnpm clean:test-applications', { env, cwd: __dirname }); await asyncExec('pnpm cache delete "@sentry/*"', { env, cwd: __dirname }); - const testAppPaths = appName ? [appName.trim()] : globSync('*', { cwd: `${__dirname}/test-applications/` }); + const testAppPaths = appName + ? [appName.trim()] + : readdirSync(join(__dirname, 'test-applications'), { withFileTypes: true }) + .filter(entry => entry.isDirectory() && !entry.name.startsWith('.')) + .map(entry => entry.name); console.log(`Runnings tests for: ${testAppPaths.join(', ')}`); console.log(''); diff --git a/dev-packages/e2e-tests/tsconfig.json b/dev-packages/e2e-tests/tsconfig.json deleted file mode 100644 index 026669f596c0..000000000000 --- a/dev-packages/e2e-tests/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "exclude": ["node_modules", "test-applications"], - "compilerOptions": { - "types": ["node"] - } -} diff --git a/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs b/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs new file mode 100644 index 000000000000..d35627adaa2d --- /dev/null +++ b/dev-packages/e2e-tests/validate-packed-tarball-setup.mjs @@ -0,0 +1,39 @@ +// @ts-check +import * as assert from 'node:assert'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getE2eTestsPackageVersion, getPackedTarballs } from './lib/packedTarballUtils.mjs'; + +const tarballs = getPackedTarballs(); + +assert.ok( + tarballs.length > 0, + `No tarballs found for version ${getE2eTestsPackageVersion()}. Run "yarn build:tarball" at the repository root.`, +); + +const packedDir = path.join(path.dirname(fileURLToPath(import.meta.url)), 'packed'); +const symlinkPaths = fs.existsSync(packedDir) + ? fs + .readdirSync(packedDir) + .filter(name => name.endsWith('-packed.tgz')) + .map(name => path.join(packedDir, name)) + : []; + +assert.ok( + symlinkPaths.length > 0, + 'No packed tarball symlinks found. Run "yarn test:prepare" in dev-packages/e2e-tests.', +); + +assert.strictEqual( + symlinkPaths.length, + tarballs.length, + `Tarball count (${tarballs.length}) does not match packed symlink count (${symlinkPaths.length}). Re-run "yarn test:prepare".`, +); + +for (const symlinkPath of symlinkPaths) { + const st = fs.lstatSync(symlinkPath); + assert.ok(st.isSymbolicLink(), `Expected ${symlinkPath} to be a symlink.`); + const target = path.resolve(path.dirname(symlinkPath), fs.readlinkSync(symlinkPath)); + assert.ok(fs.existsSync(target), `Symlink ${symlinkPath} points to missing file: ${target}`); +} diff --git a/dev-packages/e2e-tests/validate-packed-tarball-setup.ts b/dev-packages/e2e-tests/validate-packed-tarball-setup.ts deleted file mode 100644 index 18a3545f7a1a..000000000000 --- a/dev-packages/e2e-tests/validate-packed-tarball-setup.ts +++ /dev/null @@ -1,42 +0,0 @@ -import * as assert from 'assert'; -import * as fs from 'fs'; -import { sync as globSync } from 'glob'; -import * as path from 'path'; - -const repositoryRoot = path.resolve(__dirname, '../..'); - -const e2ePkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8')) as { version: string }; -const version = e2ePkg.version; - -const tarballPaths = globSync(`{packages/*,dev-packages/test-utils}/sentry-*-${version}.tgz`, { - cwd: repositoryRoot, - absolute: true, -}); - -assert.ok( - tarballPaths.length > 0, - `No tarballs found for version ${version}. Run "yarn build:tarball" at the repository root.`, -); - -const symlinkPaths = globSync('packed/*-packed.tgz', { - cwd: __dirname, - absolute: true, -}); - -assert.ok( - symlinkPaths.length > 0, - 'No packed tarball symlinks found. Run "yarn test:prepare" in dev-packages/e2e-tests.', -); - -assert.strictEqual( - symlinkPaths.length, - tarballPaths.length, - `Tarball count (${tarballPaths.length}) does not match packed symlink count (${symlinkPaths.length}). Re-run "yarn sync:packed-tarballs".`, -); - -for (const symlinkPath of symlinkPaths) { - const st = fs.lstatSync(symlinkPath); - assert.ok(st.isSymbolicLink(), `Expected ${symlinkPath} to be a symlink.`); - const target = path.resolve(path.dirname(symlinkPath), fs.readlinkSync(symlinkPath)); - assert.ok(fs.existsSync(target), `Symlink ${symlinkPath} points to missing file: ${target}`); -} diff --git a/scripts/normalize-e2e-test-dump-transaction-events.js b/scripts/normalize-e2e-test-dump-transaction-events.js index e3459d52621d..376b86d2b413 100644 --- a/scripts/normalize-e2e-test-dump-transaction-events.js +++ b/scripts/normalize-e2e-test-dump-transaction-events.js @@ -3,93 +3,91 @@ const fs = require('fs'); const path = require('path'); -const glob = require('glob'); - -glob.glob( - '../dev-packages/e2e-tests/test-applications/*/event-dumps/*.dump', - { - cwd: __dirname, - absolute: true, - }, - (err, dumpPaths) => { - if (err) { - throw err; + +const dumpDir = process.argv[2]; +if (!dumpDir) { + throw new Error('Usage: node normalize-e2e-test-dump-transaction-events.js '); +} + +const dumpPaths = fs.existsSync(dumpDir) + ? fs + .readdirSync(dumpDir) + .filter(name => name.endsWith('.dump')) + .map(name => path.join(dumpDir, name)) + : []; + +dumpPaths.forEach(dumpPath => { + const fileContents = fs.readFileSync(dumpPath, 'utf8'); + + const transactionNodes = []; + + fileContents.split('\n').forEach(serializedEnvelope => { + let envelope; + try { + envelope = JSON.parse(serializedEnvelope); + } catch { + return; + // noop } - dumpPaths.forEach(dumpPath => { - const fileContents = fs.readFileSync(dumpPath, 'utf8'); - - const transactionNodes = []; - - fileContents.split('\n').forEach(serializedEnvelope => { - let envelope; - try { - envelope = JSON.parse(serializedEnvelope); - } catch { - return; - // noop - } - - const envelopeItems = envelope[1]; - - envelopeItems.forEach(([envelopeItemHeader, transaction]) => { - if (envelopeItemHeader.type === 'transaction') { - const rootNode = { - runtime: transaction.contexts.runtime?.name, - op: transaction.contexts.trace.op, - name: transaction.transaction, - children: [], - }; - - const spanMap = new Map(); - spanMap.set(transaction.contexts.trace.span_id, rootNode); - - transaction.spans.forEach(span => { - const node = { - op: span.data['sentry.op'], - name: span.description, - parent_span_id: span.parent_span_id, - children: [], - }; - spanMap.set(span.span_id, node); - }); - - transaction.spans.forEach(span => { - const node = spanMap.get(span.span_id); - if (node?.parent_span_id) { - const parentNode = spanMap.get(node.parent_span_id); - parentNode.children.push(node); - } - }); - - transactionNodes.push(rootNode); - } + const envelopeItems = envelope[1]; + + envelopeItems.forEach(([envelopeItemHeader, transaction]) => { + if (envelopeItemHeader.type === 'transaction') { + const rootNode = { + runtime: transaction.contexts.runtime?.name, + op: transaction.contexts.trace.op, + name: transaction.transaction, + children: [], + }; + + const spanMap = new Map(); + spanMap.set(transaction.contexts.trace.span_id, rootNode); + + transaction.spans.forEach(span => { + const node = { + op: span.data['sentry.op'], + name: span.description, + parent_span_id: span.parent_span_id, + children: [], + }; + spanMap.set(span.span_id, node); }); - }); - - const output = transactionNodes - .sort((a, b) => { - const aSerialized = serializeNode(a); - const bSerialized = serializeNode(b); - if (aSerialized < bSerialized) { - return -1; - } else if (aSerialized > bSerialized) { - return 1; - } else { - return 0; + + transaction.spans.forEach(span => { + const node = spanMap.get(span.span_id); + if (node?.parent_span_id) { + const parentNode = spanMap.get(node.parent_span_id); + parentNode.children.push(node); } - }) - .map(node => buildDeterministicStringFromNode(node)) - .join('\n\n-----------------------\n\n'); - - fs.writeFileSync( - path.join(path.dirname(dumpPath), `normalized-transactions-${path.basename(dumpPath, '.dump')}.txt`), - output, - 'utf-8', - ); + }); + + transactionNodes.push(rootNode); + } }); - }, -); + }); + + const output = transactionNodes + .sort((a, b) => { + const aSerialized = serializeNode(a); + const bSerialized = serializeNode(b); + if (aSerialized < bSerialized) { + return -1; + } else if (aSerialized > bSerialized) { + return 1; + } else { + return 0; + } + }) + .map(node => buildDeterministicStringFromNode(node)) + .join('\n\n-----------------------\n\n'); + + fs.writeFileSync( + path.join(path.dirname(dumpPath), `normalized-transactions-${path.basename(dumpPath, '.dump')}.txt`), + output, + 'utf-8', + ); +}); // ------- utility fns ---------- diff --git a/yarn.lock b/yarn.lock index 6d53cd37b12a..f572b86b6aa8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14137,16 +14137,16 @@ dotenv@16.4.7: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.7.tgz#0e20c5b82950140aa99be360a8a5f52335f53c26" integrity sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ== -dotenv@17.4.2, dotenv@^17.3.1, dotenv@^17.4.2: - version "17.4.2" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" - integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== - dotenv@^16.4.5, dotenv@^16.6.1: version "16.6.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== +dotenv@^17.3.1, dotenv@^17.4.2: + version "17.4.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-17.4.2.tgz#c07e54a746e11eba021dd9e1047ced5afdc1c034" + integrity sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw== + dset@^3.1.3, dset@^3.1.4: version "3.1.4" resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248"