Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1c97b68
add orchestrion plan
mydea May 15, 2026
2c73b3f
add the plan
mydea May 15, 2026
5a326f5
Add general utils and exports to node SDK
mydea May 15, 2026
0245e9c
add mysql example
mydea May 15, 2026
273d88f
fixes
mydea May 15, 2026
94994c9
fix lint
mydea May 15, 2026
f93b1c7
fix exports
mydea May 15, 2026
6c6b27f
fix types build
mydea May 15, 2026
386fbb7
fix import
mydea May 15, 2026
731a2fd
better handle sync errors
mydea May 15, 2026
fc32803
tests
mydea May 18, 2026
0f90775
mysql fixes
mydea May 18, 2026
28b5d80
remove unused file
mydea May 18, 2026
581ea31
be defensive
mydea May 18, 2026
aab44be
improvements
mydea May 18, 2026
cf95d48
add custom output
mydea May 18, 2026
d2f0ef4
add some more size limit scenarios
mydea May 21, 2026
390fafd
add test app
mydea May 21, 2026
1b6c939
bump transformer plugin
mydea May 21, 2026
9497725
fixes
mydea May 26, 2026
17c095e
add other test app
mydea May 26, 2026
dfbfb8c
add noExternal
mydea May 26, 2026
7f80b79
fix and cjs
mydea May 26, 2026
0738034
bump tracing hooks
mydea May 26, 2026
09f432a
streamline detect
mydea May 26, 2026
7fe9763
feat(orchestrion): move into server-utils
isaacs Jun 1, 2026
aa80fbb
fix(test): update node orchestrion tests to match usage
isaacs Jun 1, 2026
3607d00
fix(orchestrion): enable import hook on deno 2.8+
isaacs Jun 7, 2026
b5aec2f
fix(server-utils): add ./orchestrion shim for submodule export
isaacs Jun 7, 2026
541e7fc
fix(server-utils): add note about not warning in production
isaacs Jun 7, 2026
eb983dc
fix(server-utils): add double-load guard to orchestrion import hook
isaacs Jun 7, 2026
7b69e3b
fix(orchestrion): remove unused submodule paths, fix sourcemaps
isaacs Jun 8, 2026
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
26 changes: 26 additions & 0 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,32 @@ module.exports = [
limit: '136 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/node (with Orchestrion)',
path: 'packages/node/build/esm/index.js',
import: createImport('init', '_experimentalSetupOrchestrion'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '173 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/node/orchestrion (ESM hook)',
path: ['node_modules/@apm-js-collab/tracing-hooks/hook.mjs', 'packages/node/build/orchestrion/import-hook.mjs'],
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '100 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/node/light',
path: 'packages/node-core/build/esm/light/index.js',
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '100 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/node - without tracing',
path: 'packages/node/build/esm/index.js',
Expand Down
486 changes: 486 additions & 0 deletions ORCHESTRIONJS_PLAN.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const NODE_EXPORTS_IGNORE = [
'preloadOpenTelemetry',
// Internal helper only needed within integrations (e.g. bunRuntimeMetricsIntegration)
'_INTERNAL_normalizeCollectionInterval',
// Experimental
'_experimentalSetupOrchestrion',
'mysqlChannelIntegration',
];

const nodeExports = Object.keys(SentryNode).filter(e => !NODE_EXPORTS_IGNORE.includes(e));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "node-express-orchestrion-cjs-app",
"version": "1.0.0",
"private": true,
"type": "commonjs",
"scripts": {
"start": "node --import @sentry/node/orchestrion ./src/app.js",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml dist",
"test:build": "pnpm install",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"express": "^5.1.0",
"mysql": "2.18.1"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "file:../../packed/sentry-core-packed.tgz"
},
"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,71 @@
const Sentry = require('@sentry/node');

const client = Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
debug: !!process.env.DEBUG,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1,
_experimentalUseOrchestrion: true,
});

Sentry._experimentalSetupOrchestrion(client);

const express = require('express');
const mysql = require('mysql');

const connection = mysql.createConnection({
user: 'root',
password: 'docker',
});

const app = express();
const port = 3030;

app.get('/test-success', function (req, res) {
res.send({ version: 'v1' });
});

app.get('/test-param/:param', function (req, res) {
res.send({ paramWas: req.params.param });
});

app.get('/test-mysql', function (req, res) {
connection.query('SELECT 1 + 1 AS solution', function () {
connection.query('SELECT NOW()', ['1', '2'], () => {
res.send({ status: 'ok' });
});
});
});
Comment thread
mydea marked this conversation as resolved.
Dismissed

app.get('/test-transaction', function (_req, res) {
Sentry.startSpan({ name: 'test-span' }, () => undefined);

res.send({ status: 'ok' });
});

app.get('/test-error', async function (req, res) {
const exceptionId = Sentry.captureException(new Error('This is an error'));

await Sentry.flush(2000);

res.send({ exceptionId });
});

app.get('/test-exception/:id', function (req, _res) {
throw new Error(`This is an exception with id ${req.params.id}`);
});

Sentry.setupExpressErrorHandler(app);

// @ts-ignore
app.use(function onError(err, req, res, next) {
// The error id is attached to `res.sentry` to be returned
// and optionally displayed to the user for support.
res.statusCode = 500;
res.end(res.sentry + '\n');
});

app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
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: 'node-express-orchestrion-cjs',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, test } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';

test('Sends correct error event', async ({ baseURL }) => {
const errorEventPromise = waitForError('node-express-orchestrion-cjs', event => {
return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
});

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

const errorEvent = await errorEventPromise;

expect(errorEvent.exception?.values).toHaveLength(1);
expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');

expect(errorEvent.request).toEqual({
method: 'GET',
cookies: {},
headers: expect.any(Object),
url: 'http://localhost:3030/test-exception/123',
});

expect(errorEvent.transaction).toEqual('GET /test-exception/:id');

expect(errorEvent.contexts?.trace).toEqual({
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Sends an API route transaction', async ({ baseURL }) => {
const pageloadTransactionEventPromise = waitForTransaction('node-express-orchestrion-cjs', transactionEvent => {
return (
transactionEvent?.contexts?.trace?.op === 'http.server' &&
transactionEvent?.transaction === 'GET /test-transaction'
);
});

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

const transactionEvent = await pageloadTransactionEventPromise;

expect(transactionEvent.contexts?.trace).toEqual({
data: {
'sentry.source': 'route',
'sentry.origin': 'auto.http.otel.http',
'sentry.op': 'http.server',
'sentry.sample_rate': 1,
url: 'http://localhost:3030/test-transaction',
'otel.kind': 'SERVER',
'http.response.status_code': 200,
'http.url': 'http://localhost:3030/test-transaction',
'http.host': 'localhost:3030',
'net.host.name': 'localhost',
'http.method': 'GET',
'http.scheme': 'http',
'http.target': '/test-transaction',
'http.user_agent': 'node',
'http.flavor': '1.1',
'net.transport': 'ip_tcp',
'net.host.ip': expect.any(String),
'net.host.port': expect.any(Number),
'net.peer.ip': expect.any(String),
'net.peer.port': expect.any(Number),
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
'http.request.header.accept': '*/*',
'http.request.header.accept_encoding': 'gzip, deflate',
'http.request.header.accept_language': '*',
'http.request.header.connection': 'keep-alive',
'http.request.header.host': expect.any(String),
'http.request.header.sec_fetch_mode': 'cors',
'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
status: 'ok',
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
origin: 'auto.http.otel.http',
});

expect(transactionEvent.contexts?.response).toEqual({
status_code: 200,
});

expect(transactionEvent).toEqual(
expect.objectContaining({
transaction: 'GET /test-transaction',
type: 'transaction',
transaction_info: {
source: 'route',
},
}),
);

const spans = transactionEvent.spans || [];

// Manually started span
expect(spans).toContainEqual({
data: { 'sentry.origin': 'manual' },
description: 'test-span',
origin: 'manual',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
});

// auto instrumented span
expect(spans).toContainEqual({
data: {
'sentry.origin': 'auto.http.express',
'sentry.op': 'request_handler.express',
'http.route': '/test-transaction',
'express.name': '/test-transaction',
'express.type': 'request_handler',
},
description: '/test-transaction',
op: 'request_handler.express',
origin: 'auto.http.express',
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
span_id: expect.stringMatching(/[a-f0-9]{16}/),
start_timestamp: expect.any(Number),
status: 'ok',
timestamp: expect.any(Number),
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
});
});

test('Sends an API route transaction for an errored route', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('node-express-orchestrion-cjs', transactionEvent => {
return (
transactionEvent.contexts?.trace?.op === 'http.server' &&
transactionEvent.transaction === 'GET /test-exception/:id' &&
transactionEvent.request?.url === 'http://localhost:3030/test-exception/777'
);
});

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
expect(transactionEvent.transaction).toEqual('GET /test-exception/:id');
expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error');
expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500);
});

test('Instruments MySQL via Orchestrion', async ({ baseURL }) => {
const transactionEventPromise = waitForTransaction('node-express-orchestrion-cjs', transactionEvent => {
return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /test-mysql';
});

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

const transactionEvent = await transactionEventPromise;

expect(transactionEvent.contexts?.trace?.op).toEqual('http.server');
expect(transactionEvent.transaction).toEqual('GET /test-mysql');
expect(transactionEvent.contexts?.trace?.status).toEqual('ok');
expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(200);

const spans = transactionEvent.spans || [];
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.mysql',
description: 'SELECT 1 + 1 AS solution',
}),
);
expect(spans).toContainEqual(
expect.objectContaining({
op: 'db',
origin: 'auto.db.orchestrion.mysql',
description: 'SELECT NOW()',
}),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "node-express-orchestrion-vite-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"start": "node --import ./dist/instrument.js ./dist/app.js",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml dist",
"test:build": "pnpm install && pnpm build",
"test:assert": "pnpm test"
},
"dependencies": {
"@sentry/node": "file:../../packed/sentry-node-packed.tgz",
"@types/express": "^4.17.21",
"@types/node": "^18.19.1",
"express": "^5.1.0",
"mysql": "2.18.1",
"typescript": "~5.0.0"
},
"devDependencies": {
"@playwright/test": "~1.56.0",
"@sentry-internal/test-utils": "link:../../../test-utils",
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"vite": "^5.4.11"
},
"resolutions": {
"@types/qs": "6.9.17"
},
"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;
Loading
Loading