diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c2974d4313e..b7775e2c45d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -993,6 +993,7 @@ jobs: [ 'angular-17', 'angular-18', + 'aws-lambda-layer', 'cloudflare-astro', 'node-express', 'create-react-app', diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/.npmrc b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/package.json b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/package.json new file mode 100644 index 000000000000..4d41ba051e4b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/package.json @@ -0,0 +1,23 @@ +{ + "name": "node-express-app", + "version": "1.0.0", + "private": true, + "scripts": { + "copy:layer": "cp -r ./../../../../packages/aws-serverless/build/aws/dist-serverless/nodejs/node_modules/ ./node_modules", + "start": "node src/run.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm copy:layer", + "test:assert": "pnpm test" + }, + "dependencies": { + }, + "devDependencies": { + "@sentry-internal/test-utils": "link:../../../test-utils", + "@playwright/test": "^1.41.1", + "wait-port": "1.0.4" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/playwright.config.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/playwright.config.ts new file mode 100644 index 000000000000..7b14daadc6d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/playwright.config.ts @@ -0,0 +1,79 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + +const eventProxyPort = 3031; +const lambdaPort = 3030; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: `http://localhost:${lambdaPort}`, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: [ + { + command: `node start-event-proxy.mjs && pnpm wait-port ${eventProxyPort}`, + port: eventProxyPort, + stdout: 'pipe', + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js new file mode 100644 index 000000000000..aa8f236b742d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/lambda-function.js @@ -0,0 +1,19 @@ +const Sentry = require('@sentry/aws-serverless'); + +const http = require('http'); + +function handle() { + Sentry.startSpanManual({ name: 'aws-lambda-layer-test-txn', op: 'test' }, span => { + http.get('http://example.com', res => { + res.on('data', d => { + process.stdout.write(d); + }); + + res.on('end', () => { + span.end(); + }); + }); + }); +} + +module.exports = { handle }; diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js new file mode 100644 index 000000000000..5e573c484637 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run-lambda.js @@ -0,0 +1,2 @@ +const { handle } = require('./lambda-function'); +handle(); diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js new file mode 100644 index 000000000000..2a99cff2d48e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/src/run.js @@ -0,0 +1,16 @@ +const child_process = require('child_process'); + +child_process.execSync('node ./src/run-lambda.js', { + stdio: 'inherit', + env: { + ...process.env, + LAMBDA_TASK_ROOT: '.', + _HANDLER: 'handle', + + NODE_OPTIONS: '--require @sentry/aws-serverless/dist/awslambda-auto', + SENTRY_DSN: 'http://public@localhost:3031/1337', + SENTRY_TRACES_SAMPLE_RATE: '1.0', + SENTRY_DEBUG: 'true', + }, + cwd: process.cwd(), +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs new file mode 100644 index 000000000000..e64e99cda75b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/start-event-proxy.mjs @@ -0,0 +1,7 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'aws-serverless-lambda-layer', + forwardToSentry: false, +}); diff --git a/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts new file mode 100644 index 000000000000..7f7f5ec1854c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/aws-lambda-layer/tests/basic.test.ts @@ -0,0 +1,36 @@ +import * as child_process from 'child_process'; +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Lambda layer SDK bundle sends events', async ({ request }) => { + const transactionEventPromise = waitForTransaction('aws-serverless-lambda-layer', transactionEvent => { + return transactionEvent?.transaction === 'aws-lambda-layer-test-txn'; + }); + + await new Promise(resolve => + setTimeout(() => { + resolve(); + }, 1000), + ); + + child_process.execSync('pnpm start', { + stdio: 'ignore', + }); + + const transactionEvent = await transactionEventPromise; + + // shows the SDK sent a transaction + expect(transactionEvent.transaction).toEqual('aws-lambda-layer-test-txn'); + + // shows that the Otel Http instrumentation is working + expect(transactionEvent.spans).toHaveLength(1); + expect(transactionEvent.spans![0]).toMatchObject({ + data: expect.objectContaining({ + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.http', + url: 'http://example.com/', + }), + description: 'GET http://example.com/', + op: 'http.client', + }); +}); diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index bb3eda188176..1274f77062bd 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -17,6 +17,11 @@ interface EventProxyServerOptions { port: number; /** The name for the proxy server used for referencing it with listener functions */ proxyServerName: string; + /** + * Whether or not to forward the event to sentry. @default `true` + * This is helpful when you can't register a tunnel in the SDK setup (e.g. lambda layer without Sentry.init call) + */ + forwardToSentry?: boolean; } interface SentryRequestCallbackData { @@ -56,7 +61,9 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P const envelopeHeader: EnvelopeItem[0] = JSON.parse(proxyRequestBody.split('\n')[0]); - if (!envelopeHeader.dsn) { + const shouldForwardEventToSentry = options.forwardToSentry != null ? options.forwardToSentry : true; + + if (!envelopeHeader.dsn && shouldForwardEventToSentry) { // eslint-disable-next-line no-console console.log( '[event-proxy-server] Warn: No dsn on envelope header. Maybe a client-report was received. Proxy request body:', @@ -69,6 +76,23 @@ export async function startEventProxyServer(options: EventProxyServerOptions): P return; } + if (!shouldForwardEventToSentry) { + const data: SentryRequestCallbackData = { + envelope: parseEnvelope(proxyRequestBody), + rawProxyRequestBody: proxyRequestBody, + rawSentryResponseBody: '', + sentryResponseStatusCode: 200, + }; + eventCallbackListeners.forEach(listener => { + listener(Buffer.from(JSON.stringify(data)).toString('base64')); + }); + + proxyResponse.writeHead(200); + proxyResponse.write('{}', 'utf-8'); + proxyResponse.end(); + return; + } + const { origin, pathname, host } = new URL(envelopeHeader.dsn as string); const projectId = pathname.substring(1); @@ -269,7 +293,13 @@ async function registerCallbackServerPort(serverName: string, port: string): Pro await writeFile(tmpFilePath, port, { encoding: 'utf8' }); } -function retrieveCallbackServerPort(serverName: string): Promise { - const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); - return readFile(tmpFilePath, 'utf8'); +async function retrieveCallbackServerPort(serverName: string): Promise { + try { + const tmpFilePath = path.join(os.tmpdir(), `${TEMP_FILE_PREFIX}${serverName}`); + return await readFile(tmpFilePath, 'utf8'); + } catch (e) { + // eslint-disable-next-line no-console + console.log('Could not read callback server port', e); + throw e; + } } diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 81c6a0a34c01..765f523b7c5c 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -76,9 +76,10 @@ }, "scripts": { "build": "run-p build:transpile build:types build:bundle", - "build:bundle": "yarn ts-node scripts/buildLambdaLayer.ts", + "build:bundle": "yarn build:layer", + "build:layer": "yarn ts-node scripts/buildLambdaLayer.ts", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:transpile": "rollup -c rollup.npm.config.mjs && yarn build:layer", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8",