Skip to content
Closed
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
7 changes: 5 additions & 2 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareHandler =
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;

// if there is an active span, we just want to enhance it with routing data etc.
if (rootSpan && spanToJSON(rootSpan).attributes[SENTRY_OP] === 'http.server') {
if (rootSpan && spanToJSON(rootSpan).attributes[SENTRY_OP] === HTTP_SERVER) {
return enhanceHttpServerSpan(ctx, next, rootSpan);
}

Expand Down Expand Up @@ -252,7 +252,10 @@ async function instrumentRequestStartHttpServerSpan(

const res = await startSpan(
{
attributes,
attributes: {
[SENTRY_OP]: HTTP_SERVER,
...attributes,
},
name,
},
async span => {
Expand Down
11 changes: 7 additions & 4 deletions packages/browser-utils/src/performance/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ export function startTrackingLongTasks(): void {

startAndEndSpan(parent, startTime, startTime + duration, {
name: 'Main UI thread blocked',
op: UI_LONG_TASK,
attributes: {
[SENTRY_OP]: UI_LONG_TASK,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
},
});
Expand Down Expand Up @@ -161,6 +161,7 @@ export function startTrackingLongAnimationFrames(): void {
const duration = msToSec(entry.duration);

const attributes: SpanAttributes = {
[SENTRY_OP]: UI_LONG_ANIMATION_FRAME,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics',
};

Expand All @@ -180,7 +181,6 @@ export function startTrackingLongAnimationFrames(): void {

startAndEndSpan(parent, startTime, startTime + duration, {
name: 'Main UI thread blocked',
op: UI_LONG_ANIMATION_FRAME,
attributes,
});
}
Expand Down Expand Up @@ -464,7 +464,11 @@ export function _addResourceSpans(
['deliveryType', 'http.response_delivery_type'],
]);

const attributesWithResourceTiming: SpanAttributes = { ...attributes, ...resourceTimingToSpanAttributes(entry) };
const attributesWithResourceTiming: SpanAttributes = {
[SENTRY_OP]: op,
...attributes,
...resourceTimingToSpanAttributes(entry),
};

const startTimestamp = timeOrigin + startTime;
const endTimestamp = startTimestamp + duration;
Expand All @@ -474,7 +478,6 @@ export function _addResourceSpans(
name: spanStreamingEnabled
? domain || RESOURCE_SPAN_NAME_FALLBACK
: resourceUrl.replace(WINDOW.location.origin, ''),
op,
attributes: attributesWithResourceTiming,
});
}
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/performance/userTiming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export function _addUserTimingSpan(
const spanEndTimestamp = originalStartTimestamp + duration;

const attributes: SpanAttributes = {
[SENTRY_OP]: entry.entryType,
[SENTRY_ORIGIN]: `auto.browser.user_timing.${entry.entryType}`,
};

Expand All @@ -130,7 +131,6 @@ export function _addUserTimingSpan(
if (spanStartTimestamp <= spanEndTimestamp) {
startAndEndSpan(parentSpan, spanStartTimestamp, spanEndTimestamp, {
name: entry.name,
op: entry.entryType,
attributes,
});
}
Expand Down
24 changes: 16 additions & 8 deletions packages/browser/src/tracing/browserTracingIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,14 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption

/** Create routing idle transaction. */
function _createRouteSpan(client: Client, startSpanOptions: StartSpanOptions, makeActive = true, url?: string): void {
const isPageloadSpan = startSpanOptions.op === 'pageload';
const originalOp = startSpanOptions.attributes?.[SENTRY_OP];
// backfill top-level `op` option
// oxlint-disable-next-line typescript/no-deprecated
const optionsWithOp: StartSpanOptions = { op: originalOp, ...startSpanOptions };
const isPageloadSpan = originalOp === PAGELOAD;

const initialSpanName = startSpanOptions.name;
const finalStartSpanOptions: StartSpanOptions = beforeStartSpan
? beforeStartSpan(startSpanOptions)
: startSpanOptions;
const initialSpanName = optionsWithOp.name;
const finalStartSpanOptions: StartSpanOptions = beforeStartSpan ? beforeStartSpan(optionsWithOp) : optionsWithOp;

// For navigations, `url` is the destination URL, so we use it to reflect the post-navigation location.
// For pageloads (and manual navigation spans without a URL) we fall back to the current location.
Expand All @@ -348,6 +350,12 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
...finalStartSpanOptions.attributes,
};

// oxlint-disable-next-line typescript/no-deprecated
if (finalStartSpanOptions.op !== originalOp) {
// oxlint-disable-next-line typescript/no-deprecated
attributes[SENTRY_OP] = finalStartSpanOptions.op;
}
Comment on lines +353 to +357

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: If a beforeStartSpan callback returns an options object without the op field, the span's operation (SENTRY_OP) attribute is incorrectly deleted, breaking UI categorization.
Severity: HIGH

Suggested Fix

Modify the condition to ensure finalStartSpanOptions.op is defined before comparing it to originalOp. For example: if (finalStartSpanOptions.op !== undefined && finalStartSpanOptions.op !== originalOp).

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/browser/src/tracing/browserTracingIntegration.ts#L354-L358

Potential issue: When a `beforeStartSpan` callback returns a new options object that
omits the deprecated `op` field, the check `finalStartSpanOptions.op !== originalOp`
incorrectly evaluates to `true` because `finalStartSpanOptions.op` is `undefined`. This
leads to `attributes[SENTRY_OP]` being set to `undefined`. Subsequently, when
`SentrySpan.setAttribute` is called with an `undefined` value, it deletes the
`SENTRY_OP` key from the span's attributes. This causes pageload and navigation spans to
lose their operation type, which breaks filtering and categorization in the Sentry UI.
This is triggered by a common usage pattern recommended in documentation.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment thread
cursor[bot] marked this conversation as resolved.

// If `finalStartSpanOptions.name` is different than `startSpanOptions.name`
// it is because `beforeStartSpan` set a custom name. Therefore we set the source to 'custom'.
if (initialSpanName !== finalStartSpanOptions.name) {
Expand Down Expand Up @@ -485,8 +493,8 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
_createRouteSpan(
client,
{
op: NAVIGATION_REDIRECT,
...startSpanOptions,
attributes: { [SENTRY_OP]: NAVIGATION_REDIRECT, ...startSpanOptions.attributes },
},
false,
navigationOptions.url,
Expand Down Expand Up @@ -517,8 +525,8 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
_createRouteSpan(
client,
{
op: NAVIGATION,
...startSpanOptions,
attributes: { [SENTRY_OP]: NAVIGATION, ...startSpanOptions.attributes },
// Navigation starts a new trace and is NOT parented under any active interaction (e.g. ui.action.click)
parentSpan: null,
},
Expand Down Expand Up @@ -555,8 +563,8 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
});

_createRouteSpan(client, {
op: PAGELOAD,
...startSpanOptions,
attributes: { [SENTRY_OP]: PAGELOAD, ...startSpanOptions.attributes },
});
});

Expand Down
2 changes: 2 additions & 0 deletions packages/bundler-plugins/src/core/build-plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export function createSentryBuildPluginManager(
Only use this if you need to manually inject debug IDs into the build artifacts.
*/
async injectDebugIds(buildArtifactPaths: string[]) {
// oxlint-disable-next-line typescript/no-deprecated
await startSpan({ name: 'inject-debug-ids', scope: sentryScope, forceTransaction: true }, async () => {
try {
const cliInstance = new SentryCliAdapter(options);
Expand Down Expand Up @@ -561,6 +562,7 @@ export function createSentryBuildPluginManager(

await startSpan(
// This is `forceTransaction`ed because this span is used in dashboards in the form of indexed transactions.
// oxlint-disable-next-line typescript/no-deprecated
{ name: 'debug-id-sourcemap-upload', scope: sentryScope, forceTransaction: true },
async () => {
// If we're not using a temp folder, we must not prepare artifacts in-place (to avoid mutating user files)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SyncKvStorage } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import type { SyncKvStorage } from '@cloudflare/workers-types';
import { DB } from '@sentry/conventions/op';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startSpan } from '@sentry/core';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SqlStorage } from '@cloudflare/workers-types';
import { SENTRY_OP } from '@sentry/conventions/attributes';
import type { SqlStorage } from '@cloudflare/workers-types';
import { DB_QUERY } from '@sentry/conventions/op';
import {
_INTERNAL_getSqlQuerySummary,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ function createSpanOptions(bindingName: string, r2Op: R2OperationKey, key?: stri
const requestKey = Array.isArray(key) ? key.join(', ') : typeof key === 'string' ? key : undefined;

return {
op,
name: spanName,
attributes: {
[CLOUDFLARE_R2_OPERATION]: operation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.get',
name: 'r2_get',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'GetObject',
Expand Down Expand Up @@ -112,7 +111,6 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.head',
name: 'r2_head',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'HeadObject',
Expand Down Expand Up @@ -142,7 +140,6 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.put',
name: 'r2_put',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'PutObject',
Expand Down Expand Up @@ -170,7 +167,6 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.delete',
name: 'r2_delete',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'DeleteObject',
Expand Down Expand Up @@ -206,7 +202,6 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.list',
name: 'r2_list',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'ListObjects',
Expand Down Expand Up @@ -236,11 +231,11 @@ describe('instrumentR2Bucket', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
op: 'object.multipart_upload.create',
name: 'r2_createMultipartUpload',
attributes: expect.objectContaining({
'cloudflare.r2.operation': 'CreateMultipartUpload',
'cloudflare.r2.request.key': 'big-file.bin',
'sentry.op': 'object.multipart_upload.create',
}),
}),
expect.any(Function),
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/integrations/mcp-server/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function createMcpSpan(config: McpSpanConfig): unknown {
return startSpan(
{
name: spanName,
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: true,
attributes,
},
Expand Down Expand Up @@ -211,6 +212,7 @@ export function buildMcpServerSpanConfig(

return {
name: spanName,
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: true,
attributes,
};
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/tracing/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { SUPPRESS_TRACING_KEY } from './constants';
*/
export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) => T): T {
const spanArguments = parseSentrySpanArguments(options);
// oxlint-disable-next-line typescript/no-deprecated
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;

// We still need to fork a potentially passed scope, as we set the active span on it
Expand Down Expand Up @@ -104,6 +105,7 @@ export function startSpan<T>(options: StartSpanOptions, callback: (span: Span) =
*/
export function startSpanManual<T>(options: StartSpanOptions, callback: (span: Span, finish: () => void) => T): T {
const spanArguments = parseSentrySpanArguments(options);
// oxlint-disable-next-line typescript/no-deprecated
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;

const customForkedScope = customScope?.clone();
Expand Down Expand Up @@ -150,6 +152,7 @@ export function startSpanManual<T>(options: StartSpanOptions, callback: (span: S
*/
export function startInactiveSpan(options: StartSpanOptions): Span {
const spanArguments = parseSentrySpanArguments(options);
// oxlint-disable-next-line typescript/no-deprecated
const { forceTransaction, parentSpan: customParentSpan, scope: customScope } = options;

// If `options.scope` is defined, we use this as as a wrapper,
Expand Down Expand Up @@ -442,8 +445,10 @@ function parseSentrySpanArguments(options: StartSpanOptions): SentrySpanArgument

// Fold `op` into the attributes up front so samplers see `sentry.op`; the `SentrySpan`
// constructor only adds it after the sampling decision. An explicit `sentry.op` attribute wins.
// oxlint-disable-next-line typescript/no-deprecated
if (options.op) {
initialCtx.attributes = {
// oxlint-disable-next-line typescript/no-deprecated
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: options.op,
...options.attributes,
};
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) {
[TRPC_PROCEDURE_PATH]: String(path),
[TRPC_PROCEDURE_TYPE]: String(type),
},
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: !!options.forceTransaction,
},
async span => {
Expand Down
40 changes: 39 additions & 1 deletion packages/core/src/types/startSpanOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,19 @@ export interface StartSpanOptions {
/** If set to true, only start a span if a parent span exists. */
onlyIfParent?: boolean;

/** An op for the span. This is a categorization for spans. */
/**
* An op for the span. This is a categorization for spans.
*
* @deprecated This option will be removed in a future version of the SDK. Set the `sentry.op` attribute instead.
* If both are set, the attribute takes precedence.
*
* @example
* ```js
* Sentry.startSpan({ name: 'my-span', attributes: { 'sentry.op': 'my.op' } }, () => {
* // ...
* });
* ```
*/
op?: string;

/**
Expand All @@ -39,6 +51,32 @@ export interface StartSpanOptions {
* If set to true, this span will be forced to be treated as a transaction in the Sentry UI, if possible and applicable.
* Note that it is up to the SDK to decide how exactly the span will be sent, which may change in future SDK versions.
* It is not guaranteed that a span started with this flag set to `true` will be sent as a transaction.
*
* @deprecated This option will be removed in the next major version of the SDK. There is no longer a concrete use
* case for it: all spans are indexed and searchable in Sentry, so a span no longer needs to be a transaction to be
* queried, filtered or aggregated on. In most cases, simply drop the option. The span is still sent, just as a child
* of its parent span, if a parent span is active.
* If you do need the span to be a segment (root) span, follow the examples below:.
*
* @example Making a span a root span:
* ```js
* Sentry.withActiveSpan(null, () => {
* Sentry.startSpan({ name: 'span-that-should-be-a-root' }, () => {
* // ...
* });
* });
* ```
*
* @example Keeping the root span attached to a specific trace:
* ```js
* Sentry.continueTrace({ sentryTrace, baggage }, () =>
* Sentry.withActiveSpan(null, () =>
* Sentry.startSpan({ name: 'span-that-should-be-a-root' }, () => {
* // ...
* }),
* ),
* );
* ```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Feat PR lacks integration tests

Low Severity

Flagged because the review rules require feat PRs to include at least one integration or E2E test. This change deprecates op and forceTransaction and migrates route span creation to sentry.op, but the diff only updates existing unit tests.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 2fa39b2. Configure here.

*/
forceTransaction?: boolean;

Expand Down
1 change: 0 additions & 1 deletion packages/nestjs/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export function SentryTraced(op: string = 'function') {
descriptor.value = function (...args: unknown[]) {
return startSpan(
{
op: op,
name: propertyKey,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced',
Expand Down
1 change: 1 addition & 0 deletions packages/nestjs/src/integrations/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export function getEventSpanOptions(event: string): {
[SENTRY_OP]: FUNCTION,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.event.nestjs',
},
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: true,
};
}
Expand Down
3 changes: 0 additions & 3 deletions packages/nestjs/test/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ describe('SentryTraced decorator', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenCalledWith(
{
op: 'test-operation',
name: 'testMethod',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced',
Expand Down Expand Up @@ -70,7 +69,6 @@ describe('SentryTraced decorator', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenCalledWith(
{
op: 'function', // default value
name: 'testDefaultOp',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced',
Expand Down Expand Up @@ -103,7 +101,6 @@ describe('SentryTraced decorator', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
expect(startSpanSpy).toHaveBeenCalledWith(
{
op: 'sync-operation',
name: 'syncMethod',
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nestjs.sentry_traced',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
return await startSpan(
{
name: `serverAction/${serverActionName}`,
// oxlint-disable-next-line typescript/no-deprecated
forceTransaction: true,
attributes: {
[SENTRY_KIND]: 'server',
Expand Down
Loading
Loading