Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(core): Rethrow caught promise rejections in startSpan, startSpanManual, trace #9958

Merged
merged 11 commits into from
Dec 22, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
async function run() {
Sentry.startSpan({ name: 'parent_span' }, () => {
Sentry.startSpan({ name: 'child_span' }, () => {
// whatever a user would do here
});
});
}

run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });
const transaction = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(transaction.transaction).toBe('parent_span');
expect(transaction.spans).toBeDefined();
});

sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });
const transaction = await getFirstSentryEnvelopeRequest<Event>(page, url);

const rootSpanId = transaction?.contexts?.trace?.spanId;

expect(transaction.spans).toHaveLength(1);

const span_1 = transaction.spans?.[0];
expect(span_1?.description).toBe('child_span');
expect(span_1?.parentSpanId).toEqual(rootSpanId);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="button1" type="button">Button 1</button>

<script>
async function run() {
await Sentry.startSpan({ name: 'parent_span', op: 'test' }, async () => {
Promise.reject('Async Promise Rejection');
});
}

const button = document.getElementById('button1');
button.addEventListener('click', async () => {
await run();
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest(
'should capture a promise rejection within an async startSpan callback',
async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });
const envelopePromise = getMultipleSentryEnvelopeRequests<Event>(page, 2);

await page.goto(url);

const clickPromise = page.getByText('Button 1').click();

const [, events] = await Promise.all([clickPromise, envelopePromise]);
const [txn, err] = events[0]?.type === 'transaction' ? [events[0], events[1]] : [events[1], events[0]];
Lms24 marked this conversation as resolved.
Show resolved Hide resolved

expect(txn).toMatchObject({ transaction: 'parent_span' });

expect(err?.exception?.values?.[0]?.value).toBe(
'Non-Error promise rejection captured with value: Async Promise Rejection',
);
},
);
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="button1" type="button">Button 1</button>

<script type="module">
async function run() {
Sentry.startSpan({ name: 'parent_span', op: 'test' }, async () => {
console.log('throwing error')
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Async Thrown Error');
});
}

const button = document.getElementById('button1');
button.addEventListener('click', () => {
void run();
console.log('click');
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest(
"should capture a thrown error within an async startSpan callback that's not awaited properly",
async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}
const envelopePromise = getMultipleSentryEnvelopeRequests<Event>(page, 2);

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);

const clickPromise = page.getByText('Button 1').click();

// awaiting both events simultaneously to avoid race conditions
const [, events] = await Promise.all([clickPromise, envelopePromise]);
const [txn, err] = events[0]?.type === 'transaction' ? [events[0], events[1]] : [events[1], events[0]];

expect(txn).toMatchObject({ transaction: 'parent_span' });
expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error');
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<button id="button1" type="button">Button 1</button>

<script type="module">
async function run() {
await Promise.resolve();
await Sentry.startSpan({ name: 'parent_span', op: 'test' }, async () => {
console.log('throwing error')
Lms24 marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Async Thrown Error');
});
}

const button = document.getElementById('button1');
button.addEventListener('click', () => {
void run();
console.log('click');
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('should capture a thrown error within an async startSpan callback', async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}
const envelopePromise = getMultipleSentryEnvelopeRequests<Event>(page, 2);

const url = await getLocalTestPath({ testDir: __dirname });
await page.goto(url);

const clickPromise = page.getByText('Button 1').click();

// awaiting both events simultaneously to avoid race conditions
const [, events] = await Promise.all([clickPromise, envelopePromise]);
const [txn, err] = events[0]?.type === 'transaction' ? [events[0], events[1]] : [events[1], events[0]];

expect(txn).toMatchObject({ transaction: 'parent_span' });
expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function run() {
Sentry.startSpan({ name: 'parent_span' }, () => {
throw new Error('Sync Error');
});
}

// using `setTimeout` here because otherwise the thrown error will be
// thrown as a generic "Script Error." instead of the actual error".
setTimeout(run);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect } from '@playwright/test';
import type { Event } from '@sentry/types';

import { sentryTest } from '../../../../utils/fixtures';
import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('should capture an error within a sync startSpan callback', async ({ getLocalTestPath, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });
const gotoPromise = page.goto(url);
const envelopePromise = getMultipleSentryEnvelopeRequests<Event>(page, 2);

const [_, events] = await Promise.all([gotoPromise, envelopePromise]);

const [txn, err] = events[0]?.type === 'transaction' ? [events[0], events[1]] : [events[1], events[0]];

expect(txn).toMatchObject({ transaction: 'parent_span' });
expect(err?.exception?.values?.[0]?.value).toBe('Sync Error');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-unused-vars

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
tracesSampleRate: 1.0,
normalizeDepth: 10,
});
35 changes: 22 additions & 13 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,25 @@ export function trace<T>(
}

if (isThenable(maybePromiseResult)) {
Promise.resolve(maybePromiseResult).then(
() => {
// @ts-expect-error - the isThenable check returns the "wrong" type here
return maybePromiseResult.then(
res => {
finishAndSetSpan();
afterFinish();
return res;
},
e => {
activeSpan && activeSpan.setStatus('internal_error');
onError(e, activeSpan);
finishAndSetSpan();
afterFinish();
throw e;
},
);
} else {
finishAndSetSpan();
afterFinish();
}

finishAndSetSpan();
afterFinish();
return maybePromiseResult;
}

Expand All @@ -89,6 +91,7 @@ export function trace<T>(
export function startSpan<T>(context: TransactionContext, callback: (span: Span | undefined) => T): T {
const ctx = normalizeContext(context);

// @ts-expect-error - isThenable returns the wrong type
return withScope(scope => {
const hub = getCurrentHub();
const parentSpan = scope.getSpan();
Expand All @@ -110,19 +113,20 @@ export function startSpan<T>(context: TransactionContext, callback: (span: Span
}

if (isThenable(maybePromiseResult)) {
Promise.resolve(maybePromiseResult).then(
() => {
return maybePromiseResult.then(
res => {
finishAndSetSpan();
return res;
},
() => {
e => {
activeSpan && activeSpan.setStatus('internal_error');
finishAndSetSpan();
throw e;
},
);
} else {
finishAndSetSpan();
}

finishAndSetSpan();
return maybePromiseResult;
});
}
Expand All @@ -149,6 +153,7 @@ export function startSpanManual<T>(
): T {
const ctx = normalizeContext(context);

// @ts-expect-error - isThenable returns the wrong type
return withScope(scope => {
const hub = getCurrentHub();
const parentSpan = scope.getSpan();
Expand All @@ -169,9 +174,13 @@ export function startSpanManual<T>(
}

if (isThenable(maybePromiseResult)) {
Promise.resolve(maybePromiseResult).then(undefined, () => {
activeSpan && activeSpan.setStatus('internal_error');
});
return maybePromiseResult.then(
res => res,
e => {
activeSpan && activeSpan.setStatus('internal_error');
throw e;
},
);
}

return maybePromiseResult;
Expand Down