Skip to content

Commit c46c2c5

Browse files
committed
Add node-core integration tests
1 parent e31fc15 commit c46c2c5

File tree

245 files changed

+8870
-5
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

245 files changed

+8870
-5
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module.exports = {
2+
env: {
3+
node: true,
4+
},
5+
extends: ['../../.eslintrc.js'],
6+
overrides: [
7+
{
8+
files: ['utils/**/*.ts', 'src/**/*.ts'],
9+
parserOptions: {
10+
project: ['tsconfig.json'],
11+
sourceType: 'module',
12+
},
13+
},
14+
{
15+
files: ['suites/**/*.ts', 'suites/**/*.mjs'],
16+
parserOptions: {
17+
project: ['tsconfig.test.json'],
18+
sourceType: 'module',
19+
ecmaVersion: 'latest',
20+
},
21+
globals: {
22+
fetch: 'readonly',
23+
},
24+
rules: {
25+
'@typescript-eslint/typedef': 'off',
26+
// Explicitly allow ts-ignore with description for Node integration tests
27+
// Reason: We run these tests on TS3.8 which doesn't support `@ts-expect-error`
28+
'@typescript-eslint/ban-ts-comment': [
29+
'error',
30+
{
31+
'ts-ignore': 'allow-with-description',
32+
'ts-expect-error': true,
33+
},
34+
],
35+
// We rely on having imports after init() is called for OTEL
36+
'import/first': 'off',
37+
},
38+
},
39+
],
40+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
suites/**/tmp_*
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Integration Tests for Sentry Node.JS Core SDK with OpenTelemetry v2 dependencies
2+
3+
## Structure
4+
5+
```
6+
suites/
7+
|---- public-api/
8+
|---- captureMessage/
9+
|---- test.ts [assertions]
10+
|---- scenario.ts [Sentry initialization and test subject]
11+
|---- customTest/
12+
|---- test.ts [assertions]
13+
|---- scenario_1.ts [optional extra test scenario]
14+
|---- scenario_2.ts [optional extra test scenario]
15+
|---- server_with_mongo.ts [optional custom server]
16+
|---- server_with_postgres.ts [optional custom server]
17+
```
18+
19+
The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple
20+
folders containing test scenarios and assertions.
21+
22+
`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but
23+
`runServer` also accepts an optional `scenarioPath` argument for non-standard usage.
24+
25+
`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry
26+
requests, and assertions. Test server, interceptors and assertions are all run on the same Vitest thread.
27+
28+
### Utilities
29+
30+
`utils/` contains helpers and Sentry-specific assertions that can be used in (`test.ts`).
31+
32+
Nock interceptors are internally used to capture envelope requests by `getEnvelopeRequest` and
33+
`getMultipleEnvelopeRequest` helpers. After capturing required requests, the interceptors are removed. Nock can manually
34+
be used inside the test cases to intercept requests but should be removed before the test ends, as not to cause
35+
flakiness.
36+
37+
## Running Tests Locally
38+
39+
Tests can be run locally with:
40+
41+
`yarn test`
42+
43+
To run tests with Vitest's watch mode:
44+
45+
`yarn test:watch`
46+
47+
To filter tests by their title:
48+
49+
`yarn test -t "set different properties of a scope"`
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@sentry-internal/node-core-integration-tests",
3+
"version": "9.27.0",
4+
"license": "MIT",
5+
"engines": {
6+
"node": ">=18"
7+
},
8+
"private": true,
9+
"main": "build/cjs/index.js",
10+
"module": "build/esm/index.js",
11+
"types": "build/types/src/index.d.ts",
12+
"scripts": {
13+
"build": "run-s build:transpile build:types",
14+
"build:dev": "yarn build",
15+
"build:transpile": "rollup -c rollup.npm.config.mjs",
16+
"build:types": "tsc -p tsconfig.types.json",
17+
"clean": "rimraf -g **/node_modules && run-p clean:script",
18+
"clean:script": "node scripts/clean.js",
19+
"lint": "eslint . --format stylish",
20+
"fix": "eslint . --format stylish --fix",
21+
"type-check": "tsc",
22+
"test": "vitest run",
23+
"test:watch": "yarn test --watch"
24+
},
25+
"dependencies": {
26+
"@nestjs/common": "11.0.16",
27+
"@nestjs/core": "10.4.6",
28+
"@nestjs/platform-express": "10.4.6",
29+
"@opentelemetry/api": "^1.9.0",
30+
"@opentelemetry/context-async-hooks": "^1.30.1",
31+
"@opentelemetry/core": "^1.30.1",
32+
"@opentelemetry/instrumentation": "^0.57.2",
33+
"@opentelemetry/instrumentation-http": "0.57.2",
34+
"@opentelemetry/resources": "^1.30.1",
35+
"@opentelemetry/sdk-trace-base": "^1.30.1",
36+
"@opentelemetry/semantic-conventions": "^1.30.0",
37+
"@sentry/core": "9.30.0",
38+
"@sentry/node": "9.30.0",
39+
"body-parser": "^1.20.3",
40+
"cors": "^2.8.5",
41+
"cron": "^3.1.6",
42+
"express": "^4.21.1",
43+
"http-terminator": "^3.2.0",
44+
"nock": "^13.5.5",
45+
"node-cron": "^3.0.3",
46+
"node-schedule": "^2.1.1",
47+
"proxy": "^2.1.1",
48+
"reflect-metadata": "0.2.1",
49+
"rxjs": "^7.8.1",
50+
"winston": "^3.17.0",
51+
"yargs": "^16.2.0"
52+
},
53+
"devDependencies": {
54+
"@types/node-cron": "^3.0.11",
55+
"@types/node-schedule": "^2.1.7",
56+
"globby": "11"
57+
},
58+
"volta": {
59+
"extends": "../../package.json"
60+
}
61+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
2+
3+
export default makeNPMConfigVariants(makeBaseNPMConfig());
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const { execSync } = require('child_process');
2+
const globby = require('globby');
3+
const { dirname, join } = require('path');
4+
5+
const cwd = join(__dirname, '..');
6+
const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path)));
7+
8+
// eslint-disable-next-line no-console
9+
console.log('Cleaning up docker containers and volumes...');
10+
11+
for (const path of paths) {
12+
try {
13+
// eslint-disable-next-line no-console
14+
console.log(`docker compose down @ ${path}`);
15+
execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path });
16+
} catch (_) {
17+
//
18+
}
19+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* eslint-disable no-console */
2+
const { execSync } = require('child_process');
3+
const { join } = require('path');
4+
const { readFileSync, writeFileSync } = require('fs');
5+
6+
const cwd = join(__dirname, '../../..');
7+
8+
// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8.
9+
// We'll pin to the last version of those types that are compatible.
10+
console.log('Pinning Express types to old versions...');
11+
12+
const packageJsonPath = join(cwd, 'package.json');
13+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
14+
15+
if (!packageJson.resolutions) packageJson.resolutions = {};
16+
packageJson.resolutions['@types/express'] = '4.17.13';
17+
packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30';
18+
19+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
20+
21+
const tsVersion = '3.8';
22+
23+
console.log(`Installing typescript@${tsVersion}, and @types/node@14...`);
24+
25+
execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, {
26+
stdio: 'inherit',
27+
cwd,
28+
});
29+
30+
console.log('Removing unsupported tsconfig options...');
31+
32+
const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json');
33+
34+
const tsConfig = require(baseTscConfigPath);
35+
36+
// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :(
37+
delete tsConfig.compilerOptions.noUncheckedIndexedAccess;
38+
39+
writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core';
2+
import type { Express } from 'express';
3+
import type { AddressInfo } from 'net';
4+
5+
/**
6+
* Debug logging transport
7+
*/
8+
export function loggingTransport(_options: BaseTransportOptions): Transport {
9+
return {
10+
send(request: Envelope): Promise<TransportMakeRequestResponse> {
11+
// eslint-disable-next-line no-console
12+
console.log(JSON.stringify(request));
13+
return Promise.resolve({ statusCode: 200 });
14+
},
15+
flush(): PromiseLike<boolean> {
16+
return new Promise(resolve => setTimeout(() => resolve(true), 1000));
17+
},
18+
};
19+
}
20+
21+
/**
22+
* Starts an express server and sends the port to the runner
23+
* @param app Express app
24+
* @param port Port to start the app on. USE WITH CAUTION! By default a random port will be chosen.
25+
* Setting this port to something specific is useful for local debugging but dangerous for
26+
* CI/CD environments where port collisions can cause flakes!
27+
*/
28+
export function startExpressServerAndSendPortToRunner(
29+
app: Pick<Express, 'listen'>,
30+
port: number | undefined = undefined,
31+
): void {
32+
const server = app.listen(port || 0, () => {
33+
const address = server.address() as AddressInfo;
34+
35+
// @ts-expect-error If we write the port to the app we can read it within route handlers in tests
36+
app.port = port || address.port;
37+
38+
// eslint-disable-next-line no-console
39+
console.log(`{"port":${port || address.port}}`);
40+
});
41+
}
42+
43+
/**
44+
* Sends the port to the runner
45+
*/
46+
export function sendPortToRunner(port: number): void {
47+
// eslint-disable-next-line no-console
48+
console.log(`{"port":${port}}`);
49+
}
50+
51+
/**
52+
* Can be used to get the port of a running app, so requests can be sent to a server from within the server.
53+
*/
54+
export function getPortAppIsRunningOn(app: Express): number | undefined {
55+
// @ts-expect-error It's not defined in the types but we'd like to read it.
56+
return app.port;
57+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as Sentry from '@sentry/node-core';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { spawn } from 'child_process';
4+
import { join } from 'path';
5+
import { Worker } from 'worker_threads';
6+
import { setupOtel } from '../../../utils/setupOtel.js';
7+
8+
const __dirname = new URL('.', import.meta.url).pathname;
9+
10+
const client = Sentry.init({
11+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
12+
release: '1.0',
13+
integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })],
14+
transport: loggingTransport,
15+
});
16+
17+
setupOtel(client);
18+
19+
(async () => {
20+
await new Promise(resolve => {
21+
const child = spawn('sleep', ['a']);
22+
child.on('error', resolve);
23+
child.on('exit', resolve);
24+
});
25+
26+
await new Promise(resolve => {
27+
const worker = new Worker(join(__dirname, 'worker.mjs'));
28+
worker.on('error', resolve);
29+
worker.on('exit', resolve);
30+
});
31+
32+
throw new Error('This is a test error');
33+
})();
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Event } from '@sentry/core';
2+
import { afterAll, expect, test } from 'vitest';
3+
import { conditionalTest } from '../../../utils';
4+
import { cleanupChildProcesses, createRunner } from '../../../utils/runner';
5+
6+
const EVENT = {
7+
// and an exception that is our ANR
8+
exception: {
9+
values: [
10+
{
11+
type: 'Error',
12+
value: 'This is a test error',
13+
},
14+
],
15+
},
16+
breadcrumbs: [
17+
{
18+
timestamp: expect.any(Number),
19+
category: 'child_process',
20+
message: "Child process exited with code '1'",
21+
level: 'warning',
22+
data: {
23+
spawnfile: 'sleep',
24+
},
25+
},
26+
{
27+
timestamp: expect.any(Number),
28+
category: 'worker_thread',
29+
message: "Worker thread errored with 'Worker error'",
30+
level: 'error',
31+
data: {
32+
threadId: expect.any(Number),
33+
},
34+
},
35+
],
36+
};
37+
38+
conditionalTest({ min: 20 })('should capture process and thread breadcrumbs', () => {
39+
afterAll(() => {
40+
cleanupChildProcesses();
41+
});
42+
43+
test('ESM', async () => {
44+
await createRunner(__dirname, 'app.mjs')
45+
.withMockSentryServer()
46+
.expect({ event: EVENT as Event })
47+
.start()
48+
.completed();
49+
});
50+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
throw new Error('Worker error');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setTimeout(() => {
2+
throw new Error('Test error');
3+
}, 1000);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
setTimeout(() => {
2+
throw new Error('Test error');
3+
}, 1000);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const Sentry = require('@sentry/node-core');
2+
const { loggingTransport } = require('@sentry-internal/node-core-integration-tests');
3+
const path = require('path');
4+
const { fork } = require('child_process');
5+
const { setupOtel } = require('../../utils/setupOtel.js');
6+
7+
const client = Sentry.init({
8+
debug: true,
9+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
10+
release: '1.0',
11+
transport: loggingTransport,
12+
});
13+
14+
setupOtel(client);
15+
16+
fork(path.join(__dirname, 'child.mjs'));
17+
18+
setTimeout(() => {
19+
throw new Error('Exiting main process');
20+
}, 3000);

0 commit comments

Comments
 (0)