Skip to content

Commit 0b53508

Browse files
mydeaclaude
andcommitted
fix(remix): Read action form data concurrently with the action
`deferSpanEnd` read the form data only at `asyncEnd`, after `callRouteAction` had already returned. A channel can't delay the action promise the way the vendored instrumentation did, so the parent `requestHandler` span could finish and flush the transaction before the form-data attributes were applied and the ACTION span ended, dropping that data. Start the form-data read at span start (from a clone taken before the action consumes the body) so it overlaps the action's execution. By `asyncEnd` the read is virtually always resolved, so applying the attributes and ending the span costs a single microtask - comfortably inside the response-rendering gap before the parent span ends. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ecfffd6 commit 0b53508

1 file changed

Lines changed: 20 additions & 15 deletions

File tree

packages/remix/src/server/integrations/tracing-channel.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ interface RouteCallParams {
4040
routeId?: string;
4141
}
4242

43-
// A pre-action clone of the request, stashed at span start so the (already consumed) body is still
44-
// readable for form-data extraction when the action settles.
43+
// The in-flight form-data read, started at span start (before the action consumes the body) so it
44+
// overlaps the action's execution rather than starting after it settles.
4545
interface ActionChannelContext extends ChannelContext {
46-
_sentryClonedRequest?: Request;
46+
_sentryFormData?: Promise<FormData>;
4747
}
4848

4949
// Minimal shape of a `matchServerRoutes` entry we read.
@@ -177,10 +177,16 @@ function subscribeCallRouteAction(actionFormDataAttributes: Record<string, strin
177177
diagnosticsChannel.tracingChannel(remixChannels.REMIX_CALL_ROUTE_ACTION),
178178
data => {
179179
const params = (data.arguments[0] ?? {}) as RouteCallParams;
180-
// Only clone the request (before the action consumes its body) when form-data capture is
181-
// configured, so the body is still readable when the span ends.
182-
if (actionFormDataAttributes) {
183-
data._sentryClonedRequest = params.request?.clone();
180+
// Start reading the form data now (from a clone taken before the action consumes the body), so
181+
// it overlaps the action's execution. Unlike the patched instrumentation, a channel can't
182+
// delay the action promise, so reading only after it settles would race the parent
183+
// `requestHandler` span flushing the transaction. Reading here means the promise is (virtually
184+
// always) already resolved by `asyncEnd`, so ending the span costs a single microtask.
185+
if (actionFormDataAttributes && params.request) {
186+
const formData = params.request.clone().formData();
187+
// Attach a handler so an unconsumed rejection (e.g. the action errored) isn't unhandled.
188+
formData.catch(() => undefined);
189+
data._sentryFormData = formData;
184190
}
185191
return startInactiveSpan({
186192
name: `ACTION ${params.routeId}`,
@@ -196,18 +202,17 @@ function subscribeCallRouteAction(actionFormDataAttributes: Record<string, strin
196202
{
197203
requiresParentSpan: true,
198204
beforeSpanEnd: (span, data) => setResponseStatus(span, data.result),
199-
// When form-data capture is configured, reading it is async, so take ownership of when the
200-
// span ends: await the form-data attributes, then end (which applies the response status via
201-
// `beforeSpanEnd`). Otherwise let the helper end the span normally.
205+
// Hold the span end until the (already in-flight) form-data read resolves, then apply the
206+
// attributes and end (which sets the response status via `beforeSpanEnd`). On error, or when
207+
// capture isn't configured, let the helper end the span normally.
202208
deferSpanEnd: ({ span, data, end }) => {
203-
const clonedRequest = data._sentryClonedRequest;
204-
if (!actionFormDataAttributes || !clonedRequest || 'error' in data) {
209+
const formData = data._sentryFormData;
210+
if (!actionFormDataAttributes || !formData || 'error' in data) {
205211
return false;
206212
}
207213

208-
clonedRequest
209-
.formData()
210-
.then(formData => applyFormDataAttributes(span, formData, actionFormDataAttributes))
214+
formData
215+
.then(resolved => applyFormDataAttributes(span, resolved, actionFormDataAttributes))
211216
// Silently continue on any error. Typically happens because the action body cannot be
212217
// processed into FormData, in which case we should just continue.
213218
.catch(() => undefined)

0 commit comments

Comments
 (0)