Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { assertBundlerInstrumentation } from '@sentry-internal/test-utils';

// Drives the four built bundles (plain / plugin / plain-external / plugin-external) across the
// build-time and runtime instrumentation paths. The external variants preload the runtime hook
// of `@sentry/aws-serverless` rather than `@sentry/node`, since that is the one a Lambda user has.
assertBundlerInstrumentation('graphql', { importHook: '@sentry/aws-serverless/import' });
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Same four-way esbuild matrix as the `node-esbuild` app, but with the plugin imported from
// `@sentry/aws-serverless/esbuild`, which is how a Lambda project that bundles with esbuild (CDK,
// SAM, Serverless Framework) would use it. See `node-esbuild/build.mjs` for the variant layout.
import { rmSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
import { sentryEsbuildPlugin } from '@sentry/aws-serverless/esbuild';

const __dirname = dirname(fileURLToPath(import.meta.url));

rmSync(join(__dirname, 'dist'), { recursive: true, force: true });

// No auth/release/telemetry — we only care about the build-time transforms and defines.
const makeSentryPlugin = () =>
sentryEsbuildPlugin({
telemetry: false,
sourcemaps: { disable: true },
release: { create: false, finalize: false, inject: false },
});

function run(name, { external, plugins }) {
return build({
entryPoints: [join(__dirname, 'src', 'entry.mjs')],
outfile: join(__dirname, 'dist', name, 'main.cjs'),
bundle: true,
platform: 'node',
format: 'cjs',
external: external ? ['graphql'] : [],
minify: false,
logLevel: 'silent',
plugins,
});
}

await run('plain', { external: false, plugins: [] });
await run('plugin', { external: false, plugins: [makeSentryPlugin()] });
await run('plain-external', { external: true, plugins: [] });
await run('plugin-external', { external: true, plugins: [makeSentryPlugin()] });

// eslint-disable-next-line no-console
console.log('built plain + plugin (inlined) and plain-external + plugin-external with esbuild');
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "aws-serverless-esbuild",
"description": "ensure the esbuild plugin from @sentry/aws-serverless/esbuild build-time instruments a bundled graphql at runtime",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"clean": "npx rimraf node_modules dist pnpm-lock.yaml",
"test:build": "pnpm install && node ./build.mjs",
"test:assert": "node ./assert.mjs"
},
"dependencies": {
"@sentry/aws-serverless": "file:../../packed/sentry-aws-serverless-packed.tgz"
},
"devDependencies": {
"@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz",
"graphql": "16.9.0",
"esbuild": "0.28.2"
},
"volta": {
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
allowBuilds:
esbuild: true
minimumReleaseAge: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The workload the bundle instruments: a `graphql` query. Whether `graphql` is inlined into the
// bundle or kept external is decided per-variant by `build.mjs`.
import { buildSchema, graphql } from 'graphql';

const schema = buildSchema('type Query { hello: String }');

export async function runWorkload() {
return graphql({ schema, source: '{ hello }', rootValue: { hello: () => 'world' } });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Bundled entrypoint, run directly with `node` (no `--import` runtime hook). `Sentry.init` runs
// first so the instrumentation's channel subscriber is ready, then the workload is imported and run.
// Spans are collected via the `spanEnd` hook and written to the file named by
// `SENTRY_E2E_RESULT_FILE` for `assert.mjs` to read back. See `node-esbuild/src/entry.mjs`.
import { writeFileSync } from 'node:fs';
import * as Sentry from '@sentry/aws-serverless';

async function main() {
Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1,
// `@sentry/aws-serverless` leaves the performance integrations out by default, so the one
// that subscribes to the graphql channel has to be added for the spans to show up.
integrations: [Sentry.graphqlIntegration()],
// Isolate the build-time path: with the runtime hook off, the bundler plugin is the only possible
// injector, so a `plain` (no-plugin) build is a true negative.
enableRuntimeChannelInjection: false,
// Hermetic — never hit the network.
transport: () => ({ send: () => Promise.resolve({}), flush: () => Promise.resolve(true) }),
});

const spans = [];
Sentry.getClient()?.on('spanEnd', span => {
const json = Sentry.spanToJSON(span);
spans.push({ name: json.name, origin: json.attributes?.['sentry.origin'] });
});

const { runWorkload } = await import('./app.mjs');

let result;
await Sentry.startSpan({ name: 'workload' }, async () => {
result = await runWorkload();
});

await Sentry.flush(2000);

const resultFile = process.env.SENTRY_E2E_RESULT_FILE;
if (!resultFile) {
throw new Error('SENTRY_E2E_RESULT_FILE is required (assertBundlerInstrumentation sets it).');
}
writeFileSync(resultFile, JSON.stringify({ result, spans }));
process.exit(0);
}

void main();
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"webpack": true,
"webpackConfigPath": "webpack.config.js"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "nestjs-webpack",
"description": "ensure @sentry/nestjs/webpack works with the Nest CLI webpack builder",
"version": "0.0.1",
"private": true,
"scripts": {
"build": "nest build",
"start": "node dist/main",
"clean": "npx rimraf node_modules dist pnpm-lock.yaml",
"test": "playwright test",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@playwright/test": "1.64.1",
"@sentry-internal/test-utils": "file:../../packed/sentry-internal-test-utils-packed.tgz",
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@types/express": "^4.17.17",
"@types/node": "^18.19.1",
"ts-loader": "^9.4.3",
"tsconfig-paths": "^4.2.0",
"typescript": "~5.0.0"
},
"volta": {
"extends": "../../package.json"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getPlaywrightConfig } from '@sentry-internal/test-utils';

const config = getPlaywrightConfig({
startCommand: `pnpm start`,
});

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minimumReleaseAge: 0
overrides:
'minimatch': '10.0.1'
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Controller, Get, Param } from '@nestjs/common';
import { flush } from '@sentry/nestjs';
import { AppService } from './app.service';

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get('test-transaction')
testTransaction() {
return this.appService.testTransaction();
}

@Get('test-exception/:id')
async testException(@Param('id') id: string) {
return this.appService.testException(id);
}

@Get('flush')
async flush() {
await flush();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
imports: [SentryModule.forRoot()],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_FILTER,
useClass: SentryGlobalFilter,
},
],
})
export class AppModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/nestjs';

@Injectable()
export class AppService {
testTransaction() {
Sentry.startSpan({ name: 'test-span' }, () => {
Sentry.startSpan({ name: 'child-span' }, () => {});
});
}

testException(id: string) {
throw new Error(`This is an exception with id ${id}`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/nestjs';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Import this first
import './instrument';

// Import other modules
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

const PORT = 3030;

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(PORT);
}

bootstrap();
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { startEventProxyServer } from '@sentry-internal/test-utils';

startEventProxyServer({
port: 3031,
proxyServerName: 'nestjs-webpack',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

const APP_NAME = 'nestjs-webpack';

test('Sends an exception with the release injected by the webpack plugin', async ({ baseURL }) => {
const errorEventPromise = waitForError(APP_NAME, event => {
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
});

const response = await fetch(`${baseURL}/test-exception/123`);
expect(response.status).toBe(500);

const errorEvent = await errorEventPromise;

expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual({
handled: false,
type: 'auto.http.nestjs.global_filter',
});
expect(errorEvent.transaction).toEqual('GET /test-exception/:id');

// Set by the release injection of `sentryWebpackPlugin` from `@sentry/nestjs/webpack`.
expect(errorEvent.release).toBe('nestjs-webpack-e2e');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from '@playwright/test';
import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils';

const APP_NAME = 'nestjs-webpack';

test('Sends streamed spans for an API route', async ({ baseURL }) => {
const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /test-transaction');

await fetch(`${baseURL}/test-transaction`);

const spans = await spansPromise;
const segmentSpan = spans.find(span => span.is_segment)!;

expect(segmentSpan).toMatchObject({
name: 'GET /test-transaction',
is_segment: true,
status: 'ok',
attributes: expect.objectContaining({
'sentry.op': { type: 'string', value: 'http.server' },
'http.route': { type: 'string', value: '/test-transaction' },
'http.response.status_code': { type: 'integer', value: 200 },
}),
});

expect(spans.find(span => span.name === 'test-span')).toMatchObject({ is_segment: false });
expect(spans.find(span => span.name === 'child-span')).toMatchObject({ is_segment: false });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
"moduleResolution": "Node16"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Extends the Nest CLI's own webpack config (`nest build` with `"webpack": true` in nest-cli.json)
// with the Sentry plugin from `@sentry/nestjs/webpack`. The injected release name shows up on
// every event, which is how the tests know the plugin ran. No auth, upload or telemetry.
const { sentryWebpackPlugin } = require('@sentry/nestjs/webpack');

module.exports = function (options) {
return {
...options,
plugins: [
...options.plugins,
sentryWebpackPlugin({
telemetry: false,
sourcemaps: { disable: true },
release: { name: 'nestjs-webpack-e2e', create: false, finalize: false },
}),
],
};
};
Loading
Loading