Skip to content

Commit ea45d20

Browse files
fix(remix): Extract deferred responses correctly in root loaders. (#8305)
Co-authored-by: Abhijeet Prasad <[email protected]>
1 parent db8421c commit ea45d20

File tree

7 files changed

+100
-3
lines changed

7 files changed

+100
-3
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import type {
2727
ServerRoute,
2828
ServerRouteManifest,
2929
} from './types';
30-
import { extractData, getRequestMatch, isResponse, json, matchServerRoutes } from './vendor/response';
30+
import { extractData, getRequestMatch, isDeferredData, isResponse, json, matchServerRoutes } from './vendor/response';
3131
import { normalizeRemixRequest } from './web-fetch';
3232

3333
// Flag to track if the core request handler is instrumented.
@@ -229,6 +229,13 @@ function makeWrappedRootLoader(origLoader: DataFunction): DataFunction {
229229
const res = await origLoader.call(this, args);
230230
const traceAndBaggage = getTraceAndBaggage();
231231

232+
if (isDeferredData(res)) {
233+
return {
234+
...res.data,
235+
...traceAndBaggage,
236+
};
237+
}
238+
232239
// Note: `redirect` and `catch` responses do not have bodies to extract
233240
if (isResponse(res) && !isRedirectResponse(res) && !isCatchResponse(res)) {
234241
const data = await extractData(res);

packages/remix/src/utils/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ export interface RouteData {
6262
[routeId: string]: AppData;
6363
}
6464

65+
export type DeferredData = {
66+
data: Record<string, unknown>;
67+
init?: ResponseInit;
68+
deferredKeys: string[];
69+
subscribe(fn: (aborted: boolean, settledKey?: string) => void): () => boolean;
70+
cancel(): void;
71+
resolveData(signal: AbortSignal): Promise<boolean>;
72+
};
73+
6574
export interface MetaFunction {
6675
(args: { data: AppData; parentsData: RouteData; params: Params; location: Location }): HtmlMetaDescriptor;
6776
}

packages/remix/src/utils/vendor/response.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//
77
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
88

9-
import type { ReactRouterDomPkg, RouteMatch, ServerRoute } from '../types';
9+
import type { DeferredData, ReactRouterDomPkg, RouteMatch, ServerRoute } from '../types';
1010

1111
/**
1212
* Based on Remix Implementation
@@ -124,3 +124,18 @@ export function getRequestMatch(url: URL, matches: RouteMatch<ServerRoute>[]): R
124124

125125
return match;
126126
}
127+
128+
/**
129+
* https://github.com/remix-run/remix/blob/3e589152bc717d04e2054c31bea5a1056080d4b9/packages/remix-server-runtime/responses.ts#L75-L85
130+
*/
131+
export function isDeferredData(value: any): value is DeferredData {
132+
const deferred: DeferredData = value;
133+
return (
134+
deferred &&
135+
typeof deferred === 'object' &&
136+
typeof deferred.data === 'object' &&
137+
typeof deferred.subscribe === 'function' &&
138+
typeof deferred.cancel === 'function' &&
139+
typeof deferred.resolveData === 'function'
140+
);
141+
}

packages/remix/test/integration/app/root.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MetaFunction, LoaderFunction, json, redirect } from '@remix-run/node';
1+
import { MetaFunction, LoaderFunction, json, defer, redirect } from '@remix-run/node';
22
import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react';
33
import { withSentry } from '@sentry/remix';
44

@@ -24,6 +24,8 @@ export const loader: LoaderFunction = async ({ request }) => {
2424
};
2525
case 'json':
2626
return json({ data_one: [], data_two: 'a string' }, { headers: { 'Cache-Control': 'max-age=300' } });
27+
case 'defer':
28+
return defer({ data_one: [], data_two: 'a string' });
2729
case 'null':
2830
return null;
2931
case 'undefined':
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { defer, LoaderFunction } from '@remix-run/node';
2+
import { useLoaderData } from '@remix-run/react';
3+
4+
type LoaderData = { id: string };
5+
6+
export const loader: LoaderFunction = async ({ params: { id } }) => {
7+
return defer({
8+
id,
9+
});
10+
};
11+
12+
export default function LoaderJSONResponse() {
13+
const data = useLoaderData<LoaderData>();
14+
15+
return (
16+
<div>
17+
<h1>{data && data.id ? data.id : 'Not Found'}</h1>
18+
</div>
19+
);
20+
}

packages/remix/test/integration/test/client/root-loader.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a `J
7272
});
7373
});
7474

75+
test('should inject `sentry-trace` and `baggage` into root loader returning a deferred response', async ({ page }) => {
76+
await page.goto('/?type=defer');
77+
78+
const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page);
79+
80+
expect(sentryTrace).toEqual(expect.any(String));
81+
expect(sentryBaggage).toEqual(expect.any(String));
82+
83+
const rootData = (await getRouteData(page))['root'];
84+
85+
expect(rootData).toMatchObject({
86+
sentryTrace: sentryTrace,
87+
sentryBaggage: sentryBaggage,
88+
});
89+
});
90+
7591
test('should inject `sentry-trace` and `baggage` into root loader returning `null`.', async ({ page }) => {
7692
await page.goto('/?type=null');
7793

packages/remix/test/integration/test/server/loader.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,32 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
187187
},
188188
});
189189
});
190+
191+
it('correctly instruments a deferred loader', async () => {
192+
const env = await RemixTestEnv.init(adapter);
193+
const url = `${env.url}/loader-defer-response`;
194+
const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
195+
const transaction = envelope[2];
196+
197+
assertSentryTransaction(transaction, {
198+
transaction: 'root',
199+
transaction_info: {
200+
source: 'route',
201+
},
202+
spans: [
203+
{
204+
description: 'root',
205+
op: 'function.remix.loader',
206+
},
207+
{
208+
description: 'routes/loader-defer-response/index',
209+
op: 'function.remix.loader',
210+
},
211+
{
212+
description: 'root',
213+
op: 'function.remix.document_request',
214+
},
215+
],
216+
});
217+
});
190218
});

0 commit comments

Comments
 (0)