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

feat(core): Deprecate trace in favor of startSpan #10012

Merged
merged 3 commits into from
Jan 4, 2024
Merged
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
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export {
setTags,
setUser,
spanStatusfromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
autoDiscoverNodePerformanceMonitoringIntegrations,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export {
extractTraceparentData,
getActiveTransaction,
spanStatusfromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
makeMultiplexedTransport,
ModuleMetadata,
Expand Down
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export {
setTags,
setUser,
spanStatusfromHttpCode,
// eslint-disable-next-line deprecation/deprecation
trace,
withScope,
captureCheckIn,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export { prepareEvent } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
export { hasTracingEnabled } from './utils/hasTracingEnabled';
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
export { handleCallbackErrors } from './utils/handleCallbackErrors';
export { spanToTraceHeader } from './utils/spanUtils';
export { DEFAULT_ENVIRONMENT } from './constants';
export { ModuleMetadata } from './integrations/metadata';
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/tracing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { extractTraceparentData, getActiveTransaction } from './utils';
export { SpanStatus } from './spanstatus';
export type { SpanStatusType } from './span';
export {
// eslint-disable-next-line deprecation/deprecation
trace,
getActiveSpan,
startSpan,
Expand Down
123 changes: 35 additions & 88 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { Span, TransactionContext } from '@sentry/types';
import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils';
import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils';

import { DEBUG_BUILD } from '../debug-build';
import { getCurrentScope, withScope } from '../exports';
import type { Hub } from '../hub';
import { getCurrentHub } from '../hub';
import { handleCallbackErrors } from '../utils/handleCallbackErrors';
import { hasTracingEnabled } from '../utils/hasTracingEnabled';

/**
Expand All @@ -18,6 +19,8 @@ import { hasTracingEnabled } from '../utils/hasTracingEnabled';
*
* @internal
* @private
*
* @deprecated Use `startSpan` instead.
*/
export function trace<T>(
context: TransactionContext,
Expand All @@ -37,43 +40,18 @@ export function trace<T>(

scope.setSpan(activeSpan);

function finishAndSetSpan(): void {
activeSpan && activeSpan.end();
scope.setSpan(parentSpan);
}

let maybePromiseResult: T;
try {
maybePromiseResult = callback(activeSpan);
} catch (e) {
activeSpan && activeSpan.setStatus('internal_error');
onError(e, activeSpan);
finishAndSetSpan();
afterFinish();
throw e;
}

if (isThenable(maybePromiseResult)) {
// @ts-expect-error - the isThenable check returns the "wrong" type here
return maybePromiseResult.then(
Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I see some nextjs E2E tests are failing - can it be that this was "swallowed" before due to this (which we changed for startSpan but I think not for trace? 🤔 cc @Lms24 / @lforst

Copy link
Member

Choose a reason for hiding this comment

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

I think the failures might be legit. I don't think there was anything being swallowed. It seems like we're timing out on waiting for elements to appear on screen. Can you verify that you are returning all the values and not stalling anything? Are we returning the response here? https://github.com/getsentry/sentry-javascript/pull/10012/files#diff-6a177c0f974e405d7e55f4d0155181376d92b37749269c0904cdc6e94b17ef84L97

Copy link
Member

Choose a reason for hiding this comment

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

I added it back. Let's see.

Copy link
Member Author

Choose a reason for hiding this comment

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

OMG. good tests!

Copy link
Member

Choose a reason for hiding this comment

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

Seems like an oversight a linter should have caught...

Copy link
Member

Choose a reason for hiding this comment

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

Re added the lint rule here #10049

res => {
finishAndSetSpan();
afterFinish();
return res;
},
e => {
activeSpan && activeSpan.setStatus('internal_error');
onError(e, activeSpan);
finishAndSetSpan();
afterFinish();
throw e;
},
);
}

finishAndSetSpan();
afterFinish();
return maybePromiseResult;
return handleCallbackErrors(
() => callback(activeSpan),
error => {
activeSpan && activeSpan.setStatus('internal_error');
onError(error, activeSpan);
},
() => {
activeSpan && activeSpan.end();
scope.setSpan(parentSpan);
afterFinish();
},
);
}

/**
Expand All @@ -90,43 +68,23 @@ 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();

const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx);
scope.setSpan(activeSpan);

function finishAndSetSpan(): void {
activeSpan && activeSpan.end();
}

let maybePromiseResult: T;
try {
maybePromiseResult = callback(activeSpan);
} catch (e) {
activeSpan && activeSpan.setStatus('internal_error');
finishAndSetSpan();
throw e;
}

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

finishAndSetSpan();
return maybePromiseResult;
return handleCallbackErrors(
() => callback(activeSpan),
() => {
// Only update the span status if it hasn't been changed yet
if (activeSpan && (!activeSpan.status || activeSpan.status === 'ok')) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Small adjustment here as well to make sure we don't overwrite the span status if it was set to something else before.

Copy link
Member

Choose a reason for hiding this comment

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

I am not sure if startSpan should concern itself with the span status. I believe the OTEL equivalent primitive also doesn't do this.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, this is a "feature" that we do that automatically. Otherwise users would need to do that themselves all the time 🤔 no strong feelings, but this is what the startSpan API is supposed to do afaik cc @AbhiPrasad

Copy link
Member

Choose a reason for hiding this comment

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

given we are also an error monitoring SDK, I think it makes sense to set error status where appropriate, I think this bit of DX is useful for people.

Copy link
Member

Choose a reason for hiding this comment

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

👍

activeSpan.setStatus('internal_error');
}
},
() => activeSpan && activeSpan.end(),
);
});
}

Expand All @@ -152,7 +110,6 @@ 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 @@ -164,25 +121,15 @@ export function startSpanManual<T>(
activeSpan && activeSpan.end();
}

let maybePromiseResult: T;
try {
maybePromiseResult = callback(activeSpan, finishAndSetSpan);
} catch (e) {
activeSpan && activeSpan.setStatus('internal_error');
throw e;
}

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

return maybePromiseResult;
return handleCallbackErrors(
() => callback(activeSpan, finishAndSetSpan),
() => {
// Only update the span status if it hasn't been changed yet, and the span is not yet finished
if (activeSpan && !activeSpan.endTimestamp && (!activeSpan.status || activeSpan.status === 'ok')) {
activeSpan.setStatus('internal_error');
}
},
);
});
}

Expand Down
63 changes: 63 additions & 0 deletions packages/core/src/utils/handleCallbackErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { isThenable } from '@sentry/utils';

/**
* Wrap a callback function with error handling.
* If an error is thrown, it will be passed to the `onError` callback and re-thrown.
*
* If the return value of the function is a promise, it will be handled with `maybeHandlePromiseRejection`.
*
* If an `onFinally` callback is provided, this will be called when the callback has finished
* - so if it returns a promise, once the promise resolved/rejected,
* else once the callback has finished executing.
* The `onFinally` callback will _always_ be called, no matter if an error was thrown or not.
*/
export function handleCallbackErrors<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Fn extends () => any,
>(
fn: Fn,
onError: (error: unknown) => void,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onFinally: () => void = () => {},
): ReturnType<Fn> {
let maybePromiseResult: ReturnType<Fn>;
try {
maybePromiseResult = fn();
} catch (e) {
onError(e);
onFinally();
throw e;
}

return maybeHandlePromiseRejection(maybePromiseResult, onError, onFinally);
}

/**
* Maybe handle a promise rejection.
* This expects to be given a value that _may_ be a promise, or any other value.
* If it is a promise, and it rejects, it will call the `onError` callback.
* Other than this, it will generally return the given value as-is.
*/
function maybeHandlePromiseRejection<MaybePromise>(
value: MaybePromise,
onError: (error: unknown) => void,
onFinally: () => void,
): MaybePromise {
if (isThenable(value)) {
// @ts-expect-error - the isThenable check returns the "wrong" type here
return value.then(
res => {
onFinally();
return res;
},
e => {
onError(e);
onFinally();
throw e;
},
);
}

onFinally();
return value;
}
Loading