Skip to content

Commit 11bb8ed

Browse files
authored
test(remix): Add server-side instrumentation integration tests. (#5538)
Adds a basic set of tests to ensure `loader`, `action` and `documentRequest` functions are properly instrumented. Also, switches `node-integration-test` utilities to use `axios` to simulate requests, instead of `http` package, to easily switch between `post` and `get` requests.
1 parent b903fd1 commit 11bb8ed

File tree

10 files changed

+343
-16
lines changed

10 files changed

+343
-16
lines changed

packages/node-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@types/mysql": "^2.15.21",
2424
"@types/pg": "^8.6.5",
2525
"apollo-server": "^3.6.7",
26+
"axios": "^0.27.2",
2627
"cors": "^2.8.5",
2728
"express": "^4.17.3",
2829
"graphql": "^16.3.0",

packages/node-integration-tests/utils/index.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2-
import { parseSemver } from '@sentry/utils';
2+
import { logger, parseSemver } from '@sentry/utils';
3+
import axios from 'axios';
34
import { Express } from 'express';
45
import * as http from 'http';
56
import { RequestOptions } from 'https';
67
import nock from 'nock';
78
import * as path from 'path';
89
import { getPortPromise } from 'portfinder';
9-
1010
/**
1111
* Returns`describe` or `describe.skip` depending on allowed major versions of Node.
1212
*
@@ -33,7 +33,7 @@ export const conditionalTest = (allowedVersion: { min?: number; max?: number }):
3333
export const assertSentryEvent = (actual: Record<string, unknown>, expected: Record<string, unknown>): void => {
3434
expect(actual).toMatchObject({
3535
event_id: expect.any(String),
36-
timestamp: expect.any(Number),
36+
timestamp: expect.anything(),
3737
...expected,
3838
});
3939
};
@@ -47,8 +47,8 @@ export const assertSentryEvent = (actual: Record<string, unknown>, expected: Rec
4747
export const assertSentryTransaction = (actual: Record<string, unknown>, expected: Record<string, unknown>): void => {
4848
expect(actual).toMatchObject({
4949
event_id: expect.any(String),
50-
timestamp: expect.any(Number),
51-
start_timestamp: expect.any(Number),
50+
timestamp: expect.anything(),
51+
start_timestamp: expect.anything(),
5252
spans: expect.any(Array),
5353
type: 'transaction',
5454
...expected,
@@ -71,12 +71,18 @@ export const parseEnvelope = (body: string): Array<Record<string, unknown>> => {
7171
* @param url The url the intercepted requests will be directed to.
7272
* @param count The expected amount of requests to the envelope endpoint. If
7373
* the amount of sentrequests is lower than`count`, this function will not resolve.
74+
* @param method The method of the request. Defaults to `GET`.
7475
* @returns The intercepted envelopes.
7576
*/
76-
export const getMultipleEnvelopeRequest = async (url: string, count: number): Promise<Record<string, unknown>[][]> => {
77+
export const getMultipleEnvelopeRequest = async (
78+
url: string,
79+
count: number,
80+
method: 'get' | 'post' = 'get',
81+
): Promise<Record<string, unknown>[][]> => {
7782
const envelopes: Record<string, unknown>[][] = [];
7883

79-
return new Promise(resolve => {
84+
// eslint-disable-next-line no-async-promise-executor
85+
return new Promise(async resolve => {
8086
nock('https://dsn.ingest.sentry.io')
8187
.post('/api/1337/envelope/', body => {
8288
const envelope = parseEnvelope(body);
@@ -92,7 +98,17 @@ export const getMultipleEnvelopeRequest = async (url: string, count: number): Pr
9298
.query(true) // accept any query params - used for sentry_key param
9399
.reply(200);
94100

95-
http.get(url);
101+
try {
102+
if (method === 'get') {
103+
await axios.get(url);
104+
} else {
105+
await axios.post(url);
106+
}
107+
} catch (e) {
108+
// We sometimes expect the request to fail, but not the test.
109+
// So, we do nothing.
110+
logger.warn(e);
111+
}
96112
});
97113
};
98114

@@ -133,10 +149,14 @@ export const getAPIResponse = async (url: URL, headers?: Record<string, string>)
133149
* Intercepts and extracts a single request containing a Sentry envelope
134150
*
135151
* @param url The url the intercepted request will be directed to.
152+
* @param method The method of the request. Defaults to `GET`.
136153
* @returns The extracted envelope.
137154
*/
138-
export const getEnvelopeRequest = async (url: string): Promise<Array<Record<string, unknown>>> => {
139-
return (await getMultipleEnvelopeRequest(url, 1))[0];
155+
export const getEnvelopeRequest = async (
156+
url: string,
157+
method: 'get' | 'post' = 'get',
158+
): Promise<Array<Record<string, unknown>>> => {
159+
return (await getMultipleEnvelopeRequest(url, 1, method))[0];
140160
};
141161

142162
/**

packages/remix/test/integration/app/entry.server.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import * as Sentry from '@sentry/remix';
66
Sentry.init({
77
dsn: 'https://public@dsn.ingest.sentry.io/1337',
88
tracesSampleRate: 1,
9+
// Disabling to test series of envelopes deterministically.
10+
autoSessionTracking: false,
911
});
1012

1113
export default function handleRequest(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const meta: MetaFunction = ({ data }) => ({
1010
baggage: data.sentryBaggage,
1111
});
1212

13-
function App() {
13+
export function App() {
1414
return (
1515
<html lang="en">
1616
<head>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ActionFunction, json, redirect, LoaderFunction } from '@remix-run/node';
2+
import { useActionData } from '@remix-run/react';
3+
4+
export const loader: LoaderFunction = async ({ params: { id } }) => {
5+
if (id === '-1') {
6+
throw new Error('Unexpected Server Error from Loader');
7+
}
8+
};
9+
10+
export const action: ActionFunction = async ({ params: { id } }) => {
11+
if (id === '-1') {
12+
throw new Error('Unexpected Server Error');
13+
}
14+
15+
if (id === '-2') {
16+
// Note: This GET request triggers to the `Loader` of the URL, not the `Action`.
17+
throw redirect('/action-json-response/-1');
18+
}
19+
20+
return json({ test: 'test' });
21+
};
22+
23+
export default function ActionJSONResponse() {
24+
const data = useActionData();
25+
26+
return (
27+
<div>
28+
<h1>{data && data.test ? data.test : 'Not Found'}</h1>
29+
</div>
30+
);
31+
}

packages/remix/test/integration/app/routes/loader-json-response/$id.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
import { json, LoaderFunction } from '@remix-run/node';
1+
import { json, LoaderFunction, redirect } from '@remix-run/node';
22
import { useLoaderData } from '@remix-run/react';
33

44
type LoaderData = { id: string };
55

66
export const loader: LoaderFunction = async ({ params: { id } }) => {
7+
if (id === '-2') {
8+
throw new Error('Unexpected Server Error from Loader');
9+
}
10+
11+
if (id === '-1') {
12+
throw redirect('/loader-json-response/-2');
13+
}
14+
715
return json({
816
id,
917
});
@@ -14,7 +22,7 @@ export default function LoaderJSONResponse() {
1422

1523
return (
1624
<div>
17-
<h1>{data.id}</h1>
25+
<h1>{data && data.id ? data.id : 'Not Found'}</h1>
1826
</div>
1927
);
2028
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
assertSentryTransaction,
3+
getEnvelopeRequest,
4+
runServer,
5+
getMultipleEnvelopeRequest,
6+
assertSentryEvent,
7+
} from './utils/helpers';
8+
9+
jest.spyOn(console, 'error').mockImplementation();
10+
11+
describe('Remix API Actions', () => {
12+
it('correctly instruments a parameterized Remix API action', async () => {
13+
const baseURL = await runServer();
14+
const url = `${baseURL}/action-json-response/123123`;
15+
const envelope = await getEnvelopeRequest(url, 'post');
16+
const transaction = envelope[2];
17+
18+
assertSentryTransaction(transaction, {
19+
transaction: 'routes/action-json-response/$id',
20+
spans: [
21+
{
22+
description: 'routes/action-json-response/$id',
23+
op: 'remix.server.action',
24+
},
25+
{
26+
description: 'routes/action-json-response/$id',
27+
op: 'remix.server.loader',
28+
},
29+
{
30+
description: 'routes/action-json-response/$id',
31+
op: 'remix.server.documentRequest',
32+
},
33+
],
34+
});
35+
});
36+
37+
it('reports an error thrown from the action', async () => {
38+
const baseURL = await runServer();
39+
const url = `${baseURL}/action-json-response/-1`;
40+
41+
const [transaction, event] = await getMultipleEnvelopeRequest(url, 2, 'post');
42+
43+
assertSentryTransaction(transaction[2], {
44+
contexts: {
45+
trace: {
46+
status: 'internal_error',
47+
tags: {
48+
'http.status_code': '500',
49+
},
50+
},
51+
},
52+
});
53+
54+
assertSentryEvent(event[2], {
55+
exception: {
56+
values: [
57+
{
58+
type: 'Error',
59+
value: 'Unexpected Server Error',
60+
stacktrace: expect.any(Object),
61+
mechanism: {
62+
data: {
63+
function: 'action',
64+
},
65+
handled: true,
66+
type: 'instrument',
67+
},
68+
},
69+
],
70+
},
71+
});
72+
});
73+
74+
it('handles a thrown 500 response', async () => {
75+
const baseURL = await runServer();
76+
const url = `${baseURL}/action-json-response/-2`;
77+
78+
const [transaction_1, event, transaction_2] = await getMultipleEnvelopeRequest(url, 3, 'post');
79+
80+
assertSentryTransaction(transaction_1[2], {
81+
contexts: {
82+
trace: {
83+
op: 'http.server',
84+
status: 'ok',
85+
tags: {
86+
method: 'POST',
87+
'http.status_code': '302',
88+
},
89+
},
90+
},
91+
tags: {
92+
transaction: 'routes/action-json-response/$id',
93+
},
94+
});
95+
96+
assertSentryTransaction(transaction_2[2], {
97+
contexts: {
98+
trace: {
99+
op: 'http.server',
100+
status: 'internal_error',
101+
tags: {
102+
method: 'GET',
103+
'http.status_code': '500',
104+
},
105+
},
106+
},
107+
tags: {
108+
transaction: 'routes/action-json-response/$id',
109+
},
110+
});
111+
112+
assertSentryEvent(event[2], {
113+
exception: {
114+
values: [
115+
{
116+
type: 'Error',
117+
value: 'Unexpected Server Error from Loader',
118+
stacktrace: expect.any(Object),
119+
mechanism: {
120+
data: {
121+
function: 'loader',
122+
},
123+
handled: true,
124+
type: 'instrument',
125+
},
126+
},
127+
],
128+
},
129+
});
130+
});
131+
});

0 commit comments

Comments
 (0)