Skip to content
Draft
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
53 changes: 53 additions & 0 deletions packages/collector/test/tracing/databases/mysql/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,14 @@ app.post('/valuesAndCall', (req, res) => {
}
});

app.post('/error', (req, res) => {
if (driver === 'mysql2/promise') {
triggerErrorWithPromises(req, res);
} else {
triggerError(req, res);
}
});

app.listen(port, () => {
log(
`Listening on port: ${process.env.APP_PORT} (driver: ${driver}, access: ${accessFunction}, cluster: ${useCluster})`
Expand Down Expand Up @@ -305,6 +313,51 @@ function insertValuesWithPromisesAndCall(req, res) {
});
}

function triggerError(req, res) {
pool.getConnection((err, connection) => {
if (err) {
log('Failed to get connection', err);
res.sendStatus(500);
return;
}

// Execute an invalid SQL query to trigger an error
connection[accessFunction]('SELECT * FROM non_existent_table', queryError => {
connection.release();

if (queryError) {
log('Expected error occurred', queryError);
res.sendStatus(500);
return;
}

res.sendStatus(200);
});
});
}

function triggerErrorWithPromises(req, res) {
pool
.getConnection()
.then(connection => {
// Execute an invalid SQL query to trigger an error
wrapAccess(connection, 'SELECT * FROM non_existent_table', null, queryError => {
connection.release();

if (queryError) {
log('Expected error occurred', queryError);
res.sendStatus(500);
} else {
res.sendStatus(200);
}
});
})
.catch(err => {
log('Failed to get connection', err);
res.sendStatus(500);
});
}

function log() {
const args = Array.prototype.slice.call(arguments);
args[0] = logPrefix + args[0];
Expand Down
49 changes: 49 additions & 0 deletions packages/collector/test/tracing/databases/mysql/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,53 @@ function test(env, agentControls) {
})
);
}));

it('must replace stack trace with error stack when query fails', () =>
controls
.sendRequest({
method: 'POST',
path: '/error'
})
.then(() =>
testUtils.retry(() =>
agentControls.getSpans().then(spans => {
expect(spans.length).to.equal(2);
const entrySpan = testUtils.expectAtLeastOneMatching(spans, [
span => expect(span.n).to.equal('node.http.server'),
span => expect(span.f.e).to.equal(String(controls.getPid())),
span => expect(span.f.h).to.equal('agent-stub-uuid')
]);

const mysqlSpan = testUtils.expectAtLeastOneMatching(spans, [
span => expect(span.t).to.equal(entrySpan.t),
span => expect(span.p).to.equal(entrySpan.s),
span => expect(span.n).to.equal('mysql'),
span => expect(span.k).to.equal(constants.EXIT),
span => expect(span.f.e).to.equal(String(controls.getPid())),
span => expect(span.f.h).to.equal('agent-stub-uuid'),
span => expect(span.ec).to.equal(1),
span => expect(span.data.mysql.error).to.exist
]);

expect(mysqlSpan.stack).to.be.a('string');
// expect(mysqlSpan.stack.length).to.be.greaterThan(0);

// mysqlSpan.stack.forEach(frame => {
// expect(frame).to.have.property('m');
// expect(frame).to.have.property('c');
// expect(frame.m).to.be.a('string');
// expect(frame.c).to.be.a('string');
// });

// const hasRelevantFrame = mysqlSpan.stack.some(
// frame =>
// frame.c.includes('app.js') ||
// frame.c.includes('mysql') ||
// frame.m.includes('query') ||
// frame.m.includes('Query')
// );
// expect(hasRelevantFrame).to.be.true;
})
)
));
}
45 changes: 45 additions & 0 deletions packages/collector/test/tracing/databases/pg_native/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,51 @@ mochaSuiteFn('tracing/pg-native', function () {
);
}));

it('must replace span stack with error stack when error occurs', () =>
controls
.sendRequest({
method: 'POST',
path: '/error',
simple: false
})
.then(response => {
expect(response).to.exist;
expect(response.error).to.contain('Error: ERROR:');
expect(response.error).to.contain('relation "nonexistanttable" does not exist');

return retry(() =>
agentControls.getSpans().then(spans => {
const httpEntry = verifyHttpEntry(spans, '/error');
const pgExit = expectAtLeastOneMatching(spans, span => {
verifyPgExitBase(span, httpEntry, 'SELECT name, email FROM nonexistanttable');
expect(span.error).to.not.exist;
expect(span.ec).to.equal(1);
});

expect(pgExit.stack).to.be.a('string');
// expect(pgExit.stack.length).to.be.greaterThan(0);

// pgExit.stack.forEach(frame => {
// expect(frame).to.have.property('m');
// expect(frame).to.have.property('c');
// expect(frame.m).to.be.a('string');
// expect(frame.c).to.be.a('string');
// });

// const hasErrorFrame = pgExit.stack.some(
// frame =>
// frame.c.includes('pg-native') ||
// frame.c.includes('app.js') ||
// frame.m.includes('query') ||
// frame.m.includes('Query')
// );
// expect(hasErrorFrame).to.be.true;

verifyHttpExit(spans, httpEntry);
})
);
}));

it('must suppress', () =>
controls
.sendRequest({
Expand Down
9 changes: 7 additions & 2 deletions packages/collector/test/tracing/misc/stack_trace/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
await expressProxyControls.stop();
});

beforeEach(async () => {
await agentControls.clearReceivedTraceData();
});

beforeEach(async () => {
await agentControls.waitUntilAppIsCompletelyInitialized(expressControls.getPid());
});
Expand All @@ -107,7 +111,7 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
.then(() =>
testUtils.retry(() =>
agentControls.getSpans().then(spans => {
expect(spans.length).to.equal(6);
expect(spans.length).to.equal(3);
testUtils.expectAtLeastOneMatching(spans, [
span => expect(span.n).to.equal('node.http.server'),
span => expect(span.stack).to.have.lengthOf(0)
Expand All @@ -126,10 +130,11 @@ const mochaSuiteFn = supportedVersion(process.versions.node) ? describe : descri
.then(() =>
testUtils.retry(() =>
agentControls.getSpans().then(spans => {
expect(spans.length).to.equal(9);
expect(spans.length).to.equal(3);
testUtils.expectAtLeastOneMatching(spans, [
span => expect(span.n).to.equal('node.http.client'),
span => expect(span.k).to.equal(constants.EXIT),
span => expect(span.data.http.status).to.equal(201),
span => expect(span.stack[2].m).to.equal('fetch'),
span => expect(span.stack[2].c).to.contains('node-fetch')
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const baseUrl = `${protocol}://user:password@localhost:${process.env.SERVER_PORT
const sslDir = path.join(__dirname, '..', '..', '..', '..', 'apps', 'ssl');
const key = fs.readFileSync(path.join(sslDir, 'key'));
const cert = fs.readFileSync(path.join(sslDir, 'cert'));
const { handleErrorResponse } = require('./errorHelper');

const app = express();

Expand Down Expand Up @@ -376,6 +377,43 @@ app.get('/without-port', (req, res) => {
request.end();
});

app.get('/trigger-error', (req, res) => {
const options = {
hostname: 'localhost',
port: process.env.SERVER_PORT,
method: 'GET',
path: '/error-endpoint',
ca: cert
};

log('Initiating call to error endpoint');

const request = httpModule.request(options, response => {
let data = '';
response.on('data', chunk => {
data += chunk;
});
response.on('end', () => {
log(`Error endpoint responded with status ${response.statusCode}`);
try {
handleErrorResponse(response.statusCode, data);
res.status(response.statusCode).json({ message: 'Error endpoint called', body: data });
} catch (error) {
log('Caught error from handleErrorResponse:', error.message);

res.status(500).json({ error: error.message, stack: error.stack });
}
});
});

request.on('error', err => {
log('Error in downstream call:', err.message);
res.sendStatus(500);
});

request.end();
});

function createUrl(req, urlPath) {
const pathWithQuery = req.query.withQuery ? `${urlPath}?q1=some&pass=verysecret&q2=value` : urlPath;
return req.query.urlObject ? new URL(pathWithQuery, baseUrl) : baseUrl + pathWithQuery;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* (c) Copyright IBM Corp. 2025
*/

'use strict';

/**
* Helper function that throws an error when a 500 status is received.
* This is used to test error stack replacement in HTTP client instrumentation.
*/
function handleErrorResponse(statusCode, responseBody) {
if (statusCode >= 500) {
const error = new Error(`Server error: ${statusCode}`);
error.statusCode = statusCode;
error.responseBody = responseBody;
throw error;
}
return { statusCode, responseBody };
}

module.exports = {
handleErrorResponse
};
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ app.put('/continue', (req, res) => {
res.json({ response: 'yada yada yada' });
});

app.get('/error-endpoint', (req, res) => {
res.status(500).json({ error: 'Internal Server Error' });
});

if (process.env.APP_USES_HTTPS === 'true') {
const sslDir = path.join(__dirname, '..', '..', '..', '..', 'apps', 'ssl');
require('https')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,8 @@ function registerTests(appUsesHttps) {
expect(span.data.http.error).to.match(/Invalid URL/);
},
span => expect(span.t).to.equal(entrySpan.t),
span => expect(span.p).to.equal(entrySpan.s)
span => expect(span.p).to.equal(entrySpan.s),
span => expect(span.stack).to.be.a('string')
]);
expectExactlyOneMatching(spans, span => {
expect(span.n).to.equal('node.http.client');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function instrumentedAccessFunction(
})
.catch(error => {
span.ec = 1;
span.data.mysql.error = tracingUtil.getErrorDetails(error);
tracingUtil.setErrorStack(span, error, exports.spanName);

span.d = Date.now() - span.ts;
span.transmit();
Expand All @@ -205,7 +205,7 @@ function instrumentedAccessFunction(
function onResult(error) {
if (error) {
span.ec = 1;
span.data.mysql.error = tracingUtil.getErrorDetails(error);
tracingUtil.setErrorStack(span, error, exports.spanName);
}

span.d = Date.now() - span.ts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ function startSpanBeforeSync(ctx, originalFn, originalArgs, statement, stackTrac
function finishSpan(error, span) {
if (error) {
span.ec = 1;
span.data.pg.error = tracingUtil.getErrorDetails(error);
// Note: Instead of 'pg', we could've passed exports.spanName if they were the same,
// We can’t use spanName here because for this instr the span name is
// "postgres", but the data is stored under span.data.pg.
tracingUtil.setErrorStack(span, error, 'pg');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exports.spanName

}

span.d = Date.now() - span.ts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ function instrument(coreModule, forceHttps) {

span.d = Date.now() - span.ts;
span.ec = res.statusCode >= 500 ? 1 : 0;

// Internal: Remove/update before RFR
// Currently we don't parse anything from the body, and its not advised too
// so do not override here, only override on the event of an error catch
// if (span.ec === 1) {
// tracingUtil.setErrorStack(span, res.body.stack);
// }

span.transmit();

if (callback) {
Expand All @@ -268,6 +276,8 @@ function instrument(coreModule, forceHttps) {
};
span.d = Date.now() - span.ts;
span.ec = 1;
tracingUtil.setErrorStack(span, e, exports.spanName);

span.transmit();
throw e;
}
Expand Down Expand Up @@ -316,6 +326,8 @@ function instrument(coreModule, forceHttps) {
};
span.d = Date.now() - span.ts;
span.ec = 1;
tracingUtil.setErrorStack(span, err, exports.spanName);

span.transmit();
});
});
Expand All @@ -336,10 +348,10 @@ function constructFromUrlOpts(options, self, forceHttps) {

try {
const agent = options.agent || self.agent;
const isHttps = forceHttps || options.protocol === 'https:' || (agent?.protocol === 'https:');
const isHttps = forceHttps || options.protocol === 'https:' || agent?.protocol === 'https:';

// Use protocol-aware default port, 443 for HTTPS, 80 for HTTP
const port = options.port || options.defaultPort || (agent?.defaultPort) || (isHttps ? 443 : 80);
const port = options.port || options.defaultPort || agent?.defaultPort || (isHttps ? 443 : 80);

const protocol =
(port === 443 && 'https:') ||
Expand Down
Loading