diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/.gitignore b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/.gitignore new file mode 100644 index 000000000000..a90494aee575 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/.gitignore @@ -0,0 +1,26 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ + +test-results diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/AGENTS.md b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/AGENTS.md new file mode 100644 index 000000000000..c5ff42975b0b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/AGENTS.md @@ -0,0 +1,22 @@ +## Development + +When starting the dev server, use background mode: + +``` +astro dev --background +``` + +Manage the background server with `astro dev stop`, `astro dev status`, and `astro dev logs`. + +## Documentation + +Full documentation: https://docs.astro.build + +Consult these guides before working on related tasks: + +- [Adding pages, dynamic routes, or middleware](https://docs.astro.build/en/guides/routing/) +- [Working with Astro components](https://docs.astro.build/en/basics/astro-components/) +- [Using React, Vue, Svelte, or other framework components](https://docs.astro.build/en/guides/framework-components/) +- [Adding or managing content](https://docs.astro.build/en/guides/content-collections/) +- [Adding styles or using Tailwind](https://docs.astro.build/en/guides/styling/) +- [Supporting multiple languages](https://docs.astro.build/en/guides/internationalization/) diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs new file mode 100644 index 000000000000..bca1420e1124 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/astro.config.mjs @@ -0,0 +1,26 @@ +import node from '@astrojs/node'; +import sentry from '@sentry/astro'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; +// @ts-check +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({ + integrations: [ + sentry({ + debug: true, + sourceMapsUploadOptions: { + enabled: false, + }, + }), + ], + output: 'server', + adapter: node({ + mode: 'standalone', + }), + vite: { + // Run the orchestrion code transform on the Vite SSR bundle so instrumented + // DB drivers get `diagnostics_channel` publishers injected at build time. + plugins: [sentryOrchestrionPlugin()], + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/docker-compose.yml b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/docker-compose.yml new file mode 100644 index 000000000000..9c965901f631 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/docker-compose.yml @@ -0,0 +1,31 @@ +services: + db: + image: mysql:8.0 + restart: always + container_name: e2e-tests-astro-7-orchestrion-mysql + # The `mysql` 2.x driver doesn't speak MySQL 8's default + # `caching_sha2_password` auth, so force the legacy plugin. + command: ['--default-authentication-plugin=mysql_native_password'] + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: docker + healthcheck: + test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 10s + + redis: + image: redis:7 + restart: always + container_name: e2e-tests-astro-7-orchestrion-redis + ports: + - '6379:6379' + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 2s + timeout: 3s + retries: 30 + start_period: 5s diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-setup.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-setup.mjs new file mode 100644 index 000000000000..cb48d539c466 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-setup.mjs @@ -0,0 +1,14 @@ +import { execSync } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function globalSetup() { + // Start MySQL + Redis via Docker Compose. `--wait` blocks until the + // healthchecks in docker-compose.yml pass, so the app can connect immediately. + execSync('docker compose up -d --wait', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-teardown.mjs new file mode 100644 index 000000000000..2742279431ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/global-teardown.mjs @@ -0,0 +1,12 @@ +import { execSync } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function globalTeardown() { + execSync('docker compose down --volumes', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json new file mode 100644 index 000000000000..aeb882821dd2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/package.json @@ -0,0 +1,33 @@ +{ + "name": "astro-7-orchestrion", + "type": "module", + "version": "0.0.1", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro", + "start": "node ./dist/server/entry.mjs", + "test:build": "pnpm install && pnpm build", + "test:assert": "TEST_ENV=production playwright test" + }, + "//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels", + "dependencies": { + "@astrojs/node": "^11.0.2", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@sentry/astro": "file:../../packed/sentry-astro-packed.tgz", + "@sentry/node": "file:../../packed/sentry-node-packed.tgz", + "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", + "astro": "^7.0.6", + "ioredis": "5.10.1", + "mysql": "^2.18.1" + }, + "devDependencies": { + "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0" + }, + "volta": { + "node": "22.22.0", + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/playwright.config.mjs new file mode 100644 index 000000000000..047fc701fa50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/playwright.config.mjs @@ -0,0 +1,11 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm start', +}); + +export default { + ...config, + globalSetup: './global-setup.mjs', + globalTeardown: './global-teardown.mjs', +}; diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.ico b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.ico new file mode 100644 index 000000000000..7f48a94d1607 Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.ico differ diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.svg b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.svg new file mode 100644 index 000000000000..f157bd1c5e28 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.client.config.js new file mode 100644 index 000000000000..bc90470cef38 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.client.config.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/astro'; + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.server.config.js new file mode 100644 index 000000000000..7731c42d3f01 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/sentry.server.config.js @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/astro'; +import { experimentalUseDiagnosticsChannelInjection } from '@sentry/node'; + +// Registers the diagnostics-channel subscribers that turn the build-time +// injected channel events (from the orchestrion Vite plugin) into Sentry spans. +// Must run before Sentry.init() +experimentalUseDiagnosticsChannelInjection(); + +Sentry.init({ + dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, + environment: 'qa', + tracesSampleRate: 1.0, + tunnel: 'http://localhost:3031/', // proxy server + debug: true, +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-ioredis.astro b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-ioredis.astro new file mode 100644 index 000000000000..d5ac5b586d47 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-ioredis.astro @@ -0,0 +1,31 @@ +--- +import Redis from 'ioredis'; + +export const prerender = false; + +const redis = new Redis({ + // Don't keep retrying forever if Redis goes away (e.g. on test teardown) + maxRetriesPerRequest: 1, + retryStrategy: () => null, +}); + +let value: string | null = null; + +try { + await redis.set('test-key', 'test-value'); + value = await redis.get('test-key'); +} finally { + redis.disconnect(); +} +--- + + + + + db-ioredis + + +

ioredis queries executed

+

{value}

+ + diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-mysql.astro b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-mysql.astro new file mode 100644 index 000000000000..2272ebce10ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/db-mysql.astro @@ -0,0 +1,28 @@ +--- +import mysql from 'mysql'; + +export const prerender = false; + +const connection = mysql.createConnection({ + user: 'root', + password: 'docker', +}); + +await new Promise(resolve => { + connection.query('SELECT 1 + 1 AS solution', () => { + connection.query('SELECT NOW()', ['1', '2'], () => { + resolve(); + }); + }); +}); +--- + + + + + db-mysql + + +

mysql queries executed

+ + diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/index.astro b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/index.astro new file mode 100644 index 000000000000..bb31aa03befa --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/src/pages/index.astro @@ -0,0 +1,21 @@ +--- + +--- + + + + + + + + + Astro + + +

Astro Orchestrion E2E Test App

+ + + diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/start-event-proxy.mjs new file mode 100644 index 000000000000..c0cfa860378b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'astro-7-orchestrion', +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tests/db.test.ts new file mode 100644 index 000000000000..d5b5185b040f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tests/db.test.ts @@ -0,0 +1,87 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Instruments ioredis automatically', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('astro-7-orchestrion', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-ioredis'; + }); + + await fetch(`${baseURL}/db-ioredis`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); + expect(transactionEvent.transaction).toEqual('GET /db-ioredis'); + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.redis', + description: 'set test-key [1 other arguments]', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'redis', + 'db.statement': 'set test-key [1 other arguments]', + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.redis', + description: 'get test-key', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'redis', + 'db.statement': 'get test-key', + }), + }), + ); +}); + +test('Instruments mysql automatically', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('astro-7-orchestrion', transactionEvent => { + return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-mysql'; + }); + + await fetch(`${baseURL}/db-mysql`); + + const transactionEvent = await transactionEventPromise; + + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.mysql', + description: 'SELECT 1 + 1 AS solution', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT 1 + 1 AS solution', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.mysql', + description: 'SELECT NOW()', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT NOW()', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + }), + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tsconfig.json b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tsconfig.json new file mode 100644 index 000000000000..8bf91d3bb997 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/astro-7-orchestrion/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "astro/tsconfigs/strict", + "include": [".astro/types.d.ts", "**/*"], + "exclude": ["dist"] +}