Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
958f29e
add orchestrion plan
mydea May 15, 2026
bb0dde5
add the plan
mydea May 15, 2026
84382f9
Add general utils and exports to node SDK
mydea May 15, 2026
eae9f19
add mysql example
mydea May 15, 2026
4a66d3b
fixes
mydea May 15, 2026
41afb5e
fix lint
mydea May 15, 2026
dbb0728
fix exports
mydea May 15, 2026
8b23a86
fix types build
mydea May 15, 2026
7fd9293
fix import
mydea May 15, 2026
6ed0b6e
better handle sync errors
mydea May 15, 2026
0526eed
tests
mydea May 18, 2026
5e8c6b8
mysql fixes
mydea May 18, 2026
38e3c30
remove unused file
mydea May 18, 2026
5c41159
be defensive
mydea May 18, 2026
3f60a87
improvements
mydea May 18, 2026
331bf67
add custom output
mydea May 18, 2026
7ae8297
add some more size limit scenarios
mydea May 21, 2026
22127cb
add test app
mydea May 21, 2026
2d44bd3
bump transformer plugin
mydea May 21, 2026
9be3fee
fixes
mydea May 26, 2026
9637aa2
add other test app
mydea May 26, 2026
2bee10a
add noExternal
mydea May 26, 2026
ea834cd
fix and cjs
mydea May 26, 2026
2bea275
bump tracing hooks
mydea May 26, 2026
41fb2e9
streamline detect
mydea May 26, 2026
f58a6ed
feat(orchestrion): move into server-utils
isaacs Jun 1, 2026
bddf297
fix(test): update node orchestrion tests to match usage
isaacs Jun 1, 2026
fbe347a
fix(orchestrion): enable import hook on deno 2.8+
isaacs Jun 7, 2026
50f940b
fix(server-utils): add ./orchestrion shim for submodule export
isaacs Jun 7, 2026
0d08ccc
fix(server-utils): add note about not warning in production
isaacs Jun 7, 2026
20b6ae2
fix(server-utils): add double-load guard to orchestrion import hook
isaacs Jun 7, 2026
d7addf6
fix(orchestrion): remove unused submodule paths, fix sourcemaps
isaacs Jun 8, 2026
e27b2e9
feat(node): collapse orchestrion opt-in to a single option
isaacs Jun 14, 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
17 changes: 17 additions & 0 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,23 @@ module.exports = [
limit: '136 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
name: '@sentry/node/import (ESM hook with diagnostics-channel injection)',
path: ['node_modules/@apm-js-collab/tracing-hooks/hook.mjs', 'packages/node/build/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
@@ -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/import ./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,72 @@
const Sentry = require('@sentry/node');

// The channels are injected by `node --import @sentry/node/import` (see the
// `start` script); opting in here via `experimentalDiagnosticsChannelInjection`
// makes the SDK subscribe to them instead of using the OTel instrumentation.
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,
experimentalDiagnosticsChannelInjection: true,
});

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,26 @@
{
"name": "node-express-orchestrion-app",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"start": "node --import ./src/instrument.mjs ./src/app.mjs",
"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;
Loading
Loading