Skip to content

Commit 4a6c7a9

Browse files
committed
Improve error handling and add missing multi-wrap guard
1 parent 4aa1f1b commit 4a6c7a9

File tree

1 file changed

+40
-20
lines changed

1 file changed

+40
-20
lines changed

packages/node/src/integrations/tracing/postgresjs.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type PostgresConnectionContext = {
3636
};
3737

3838
const CONNECTION_CONTEXT_SYMBOL = Symbol('sentryPostgresConnectionContext');
39+
const INSTRUMENTED_MARKER = Symbol.for('sentry.instrumented.postgresjs');
3940

4041
type PostgresJsInstrumentationConfig = InstrumentationConfig & {
4142
/**
@@ -112,6 +113,13 @@ export class PostgresJsInstrumentation extends InstrumentationBase<PostgresJsIns
112113

113114
const WrappedPostgres = function (this: unknown, ...args: unknown[]): unknown {
114115
const sql = Reflect.construct(Original as (...args: unknown[]) => unknown, args);
116+
117+
// Validate that construction succeeded and returned a valid function object
118+
if (!sql || typeof sql !== 'function') {
119+
DEBUG_BUILD && debug.warn('postgres() did not return a valid instance');
120+
return sql;
121+
}
122+
115123
return self._instrumentSqlInstance(sql);
116124
};
117125

@@ -267,7 +275,8 @@ export class PostgresJsInstrumentation extends InstrumentationBase<PostgresJsIns
267275
*/
268276
private _instrumentSqlInstance(sql: unknown, parentConnectionContext?: PostgresConnectionContext): unknown {
269277
// Check if already instrumented to prevent double-wrapping
270-
if ((sql as { __sentryInstrumented?: boolean }).__sentryInstrumented) {
278+
// Using Symbol.for() ensures the marker survives proxying
279+
if ((sql as Record<symbol, unknown>)[INSTRUMENTED_MARKER]) {
271280
return sql;
272281
}
273282

@@ -313,8 +322,11 @@ export class PostgresJsInstrumentation extends InstrumentationBase<PostgresJsIns
313322
this._attachConnectionContext(sql, proxiedSql as Record<symbol, unknown>);
314323
}
315324

316-
// Mark as instrumented to prevent double-wrapping
317-
(sql as { __sentryInstrumented?: boolean }).__sentryInstrumented = true;
325+
// Mark both the original and proxy as instrumented to prevent double-wrapping
326+
// The proxy might be passed to other methods, or the original
327+
// might be accessed directly, so we need to mark both
328+
(sql as Record<symbol, unknown>)[INSTRUMENTED_MARKER] = true;
329+
(proxiedSql as Record<symbol, unknown>)[INSTRUMENTED_MARKER] = true;
318330

319331
return proxiedSql;
320332
}
@@ -377,6 +389,8 @@ export class PostgresJsInstrumentation extends InstrumentationBase<PostgresJsIns
377389
try {
378390
config.requestHook(span, sanitizedSqlQuery, connectionContext);
379391
} catch (e) {
392+
// Set attribute to indicate hook failure, making it visible in spans
393+
span.setAttribute('sentry.hook.error', 'requestHook failed');
380394
DEBUG_BUILD && debug.error(`Error in requestHook for ${INTEGRATION_NAME} integration:`, e);
381395
}
382396
}
@@ -388,28 +402,34 @@ export class PostgresJsInstrumentation extends InstrumentationBase<PostgresJsIns
388402

389403
queryWithCallbacks.resolve = new Proxy(queryWithCallbacks.resolve as (...args: unknown[]) => unknown, {
390404
apply: (resolveTarget, resolveThisArg, resolveArgs: [{ command?: string }]) => {
391-
const result = Reflect.apply(resolveTarget, resolveThisArg, resolveArgs);
392-
self._setOperationName(span, sanitizedSqlQuery, resolveArgs?.[0]?.command);
393-
span.end();
394-
return result;
405+
try {
406+
self._setOperationName(span, sanitizedSqlQuery, resolveArgs?.[0]?.command);
407+
span.end();
408+
} catch (e) {
409+
DEBUG_BUILD && debug.error('Error ending span in resolve callback:', e);
410+
}
411+
412+
return Reflect.apply(resolveTarget, resolveThisArg, resolveArgs);
395413
},
396414
});
397415

398416
queryWithCallbacks.reject = new Proxy(queryWithCallbacks.reject as (...args: unknown[]) => unknown, {
399417
apply: (rejectTarget, rejectThisArg, rejectArgs: { message?: string; code?: string; name?: string }[]) => {
400-
span.setStatus({
401-
code: SPAN_STATUS_ERROR,
402-
message: rejectArgs?.[0]?.message || 'unknown_error',
403-
});
404-
405-
span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'unknown');
406-
span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'unknown');
407-
408-
self._setOperationName(span, sanitizedSqlQuery);
409-
410-
const result = Reflect.apply(rejectTarget, rejectThisArg, rejectArgs);
411-
span.end();
412-
return result;
418+
try {
419+
span.setStatus({
420+
code: SPAN_STATUS_ERROR,
421+
message: rejectArgs?.[0]?.message || 'unknown_error',
422+
});
423+
424+
span.setAttribute(ATTR_DB_RESPONSE_STATUS_CODE, rejectArgs?.[0]?.code || 'unknown');
425+
span.setAttribute(ATTR_ERROR_TYPE, rejectArgs?.[0]?.name || 'unknown');
426+
427+
self._setOperationName(span, sanitizedSqlQuery);
428+
span.end();
429+
} catch (e) {
430+
DEBUG_BUILD && debug.error('Error ending span in reject callback:', e);
431+
}
432+
return Reflect.apply(rejectTarget, rejectThisArg, rejectArgs);
413433
},
414434
});
415435

0 commit comments

Comments
 (0)