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
14 changes: 12 additions & 2 deletions packages/hono/test/shared/patchAppRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('patchAppRequest', () => {
);
});

it('skips span creation when there is no active span', async () => {
it('requires a parent when starting a span', async () => {
getActiveSpanMock.mockReturnValue(undefined);

const app = new Hono();
Expand All @@ -51,7 +51,17 @@ describe('patchAppRequest', () => {

const res = await app.request('/hello');

expect(startSpanMock).not.toHaveBeenCalled();
expect(startSpanMock).toHaveBeenCalledWith(
{
name: 'GET /hello',
onlyIfParent: true,
attributes: {
'sentry.op': 'http.server',
'sentry.origin': 'auto.http.hono.internal_request',
},
},
expect.any(Function),
);
expect(await res.text()).toBe('world');
});

Expand Down
27 changes: 13 additions & 14 deletions packages/remix/src/server/integrations/tracing-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ function subscribeCallRouteLoader(): void {
// With span streaming, a `function` span is named after the function it wraps. The route id
// stays on `match.route.id`.
name: client && hasSpanStreamingEnabled(client) ? 'loader' : description,
onlyIfParent: true,
attributes: {
[SENTRY_ORIGIN]: ORIGIN,
[SENTRY_OP]: FUNCTION,
Expand All @@ -203,7 +204,6 @@ function subscribeCallRouteLoader(): void {
});
},
{
requiresParentSpan: true,
beforeSpanEnd: (span, data) => setResponseStatus(span, data.result),
},
);
Expand All @@ -214,23 +214,13 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined):
diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_ACTION),
data => {
const params = (data.arguments[0] ?? {}) as RouteCallParams;
// Start reading the form data now (from a clone taken before the action consumes the body), so
// it overlaps the action's execution. Unlike the patched instrumentation, a channel can't
// delay the action promise, so reading only after it settles would race the parent
// `requestHandler` span flushing the transaction. Reading here means the promise is (virtually
// always) already resolved by `asyncEnd`, so ending the span costs a single microtask.
if (formDataCapture && params.request) {
const formData = params.request.clone().formData();
// Attach a handler so an unconsumed rejection (e.g. the action errored) isn't unhandled.
formData.catch(() => undefined);
data._sentryFormData = formData;
}
const client = getClient();
const description = `ACTION ${params.routeId}`;
return startInactiveSpan({
const span = startInactiveSpan({
// With span streaming, a `function` span is named after the function it wraps. The route id
// stays on `match.route.id`.
name: client && hasSpanStreamingEnabled(client) ? 'action' : description,
onlyIfParent: true,
attributes: {
[SENTRY_ORIGIN]: ORIGIN,
[SENTRY_OP]: FUNCTION,
Expand All @@ -240,9 +230,18 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined):
...getMatchAttributes(params),
},
});

// Read before the action consumes the body so enrichment can finish before the request span ends.
if (span.isRecording() && formDataCapture && params.request) {
const formData = params.request.clone().formData();
// Attach a handler so an unconsumed rejection (e.g. the action errored) isn't unhandled.
formData.catch(() => undefined);
data._sentryFormData = formData;
}

return span;
},
{
requiresParentSpan: true,
beforeSpanEnd: (span, data) => setResponseStatus(span, data.result),
// Hold the span end until the (already in-flight) form-data read resolves, then apply the
// attributes and end (which sets the response status via `beforeSpanEnd`). On error, or when
Expand Down
1 change: 1 addition & 0 deletions packages/remix/test/server/tracing-channel-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export function makeSpan(data: Record<string, unknown> = {}): Span {
setAttributes: vi.fn(),
setAttribute: vi.fn(),
updateName: vi.fn(),
isRecording: () => true,
getSpanJSON: () => ({ attributes: data }),
getStaticSpanJSON: () => ({ data }),
} as unknown as Span;
Expand Down
21 changes: 18 additions & 3 deletions packages/remix/test/server/tracing-channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ describe('remixIntegration (Orchestrion-based)', () => {
beforeEach(() => {
span = makeSpan();
startInactiveSpanSpy = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span);
// A truthy active span by default, so the `requiresParentSpan` gate passes.
getActiveSpanSpy = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as Span);
});

Expand Down Expand Up @@ -112,6 +111,7 @@ describe('remixIntegration (Orchestrion-based)', () => {
expect(startInactiveSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'LOADER routes/users.$userId',
onlyIfParent: true,
attributes: expect.objectContaining({
'sentry.description': 'LOADER routes/users.$userId',
'sentry.origin': 'auto.http.remix',
Expand All @@ -129,13 +129,27 @@ describe('remixIntegration (Orchestrion-based)', () => {
expect(span.end).toHaveBeenCalledTimes(1);
});

it('callRouteLoader: does not create a span without an active parent span', async () => {
it('callRouteLoader: requires a parent when starting a span', async () => {
getActiveSpanSpy.mockReturnValue(undefined);
const ctx = { arguments: [{ routeId: 'x', request: makeRequest(), params: {} }] };

await tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER).tracePromise(async () => ({ status: 200 }), ctx);

expect(startInactiveSpanSpy).not.toHaveBeenCalled();
expect(startInactiveSpanSpy).toHaveBeenCalledWith({
name: 'LOADER x',
onlyIfParent: true,
attributes: {
'sentry.origin': 'auto.http.remix',
'sentry.op': 'function',
'sentry.description': 'LOADER x',
'code.function.name': 'loader',
'http.request.method': 'GET',
'url.full': 'http://localhost/test',
'url.path': '/test',
'match.route.id': 'x',
'router.navigation.route.id': 'x',
},
});
});

it('callRouteAction: builds an ACTION span and captures the configured form-data keys', async () => {
Expand All @@ -154,6 +168,7 @@ describe('remixIntegration (Orchestrion-based)', () => {
expect(startInactiveSpanSpy).toHaveBeenCalledWith(
expect.objectContaining({
name: 'ACTION routes/submit',
onlyIfParent: true,
attributes: expect.objectContaining({
'sentry.description': 'ACTION routes/submit',
'sentry.op': 'function',
Expand Down
10 changes: 3 additions & 7 deletions packages/server-utils/src/integrations/dataloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,7 @@ function subscribeConstruct(): void {
function subscribeLoad(): void {
const channel = diagnosticsChannel.tracingChannel<DataLoaderChannelContext>(CHANNELS.DATALOADER_LOAD);

bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0]), {
requiresParentSpan: true,
});
bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0]));

channel.end.subscribe(message => {
const data = message as TracingChannelPayloadWithSpan<DataLoaderChannelContext>;
Expand All @@ -194,10 +192,8 @@ function subscribeLoad(): void {
}

function subscribeSimpleOperation(channelName: ChannelName, operation: Operation): void {
bindTracingChannelToSpan(
diagnosticsChannel.tracingChannel<DataLoaderChannelContext>(channelName),
data => startInactiveSpanFor(data.self, operation, data.arguments[0]),
{ requiresParentSpan: true },
bindTracingChannelToSpan(diagnosticsChannel.tracingChannel<DataLoaderChannelContext>(channelName), data =>
startInactiveSpanFor(data.self, operation, data.arguments[0]),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,13 @@ function instrumentInternalRequests(): void {
).toUpperCase();
return startInactiveSpan({
name: `${method} ${extractPathname(input)}`,
onlyIfParent: true,
attributes: {
[SENTRY_OP]: HTTP_SERVER,
[SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN,
},
});
},
{ requiresParentSpan: true },
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes';
import { HTTP_SERVER } from '@sentry/conventions/op';
import { debug, getActiveSpan, getOriginalFunction, startSpan, type WrappedFunction } from '@sentry/core';
import { debug, getOriginalFunction, startSpan, type WrappedFunction } from '@sentry/core';
import { INTERNAL_REQUEST_ORIGIN } from './constants';
import type { Env, Hono } from './honoTypes';
import { DEBUG_BUILD } from '../../debug-build';
Expand Down Expand Up @@ -82,10 +82,6 @@ export function patchAppRequest<E extends Env>(app: Hono<E>): void {
apply(_target, thisArg, args: [string | Request | URL, RequestInit?, ...unknown[]]) {
const [input, requestInit, ...rest] = args;

if (!getActiveSpan()) {
return Reflect.apply(_target, thisArg, args);
}

let method = requestInit?.method ?? (input instanceof Request ? input.method : 'GET');
method = method.toUpperCase();

Expand Down
Loading