Skip to content

Commit 4ebac94

Browse files
andreiborzaLms24
andauthored
feat(solidstart): Add sentry onBeforeResponse middleware to enable distributed tracing (#13221)
Works by adding the Sentry middlware to your `src/middleware.ts` file: ```typescript import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware'; import { createMiddleware } from '@solidjs/start/middleware'; export default createMiddleware({ onBeforeResponse: [ sentryBeforeResponseMiddleware(), // Add your other middleware handlers after `sentryBeforeResponseMiddleware` ], }); ``` And specifying `./src/middleware.ts` in `app.config.ts` Closes: #12551 Co-authored-by: Lukas Stracke <[email protected]>
1 parent bedc385 commit 4ebac94

File tree

7 files changed

+194
-5
lines changed

7 files changed

+194
-5
lines changed

packages/solidstart/README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ Initialize the SDK in `entry-client.jsx`
4646

4747
```jsx
4848
import * as Sentry from '@sentry/solidstart';
49+
import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
4950
import { mount, StartClient } from '@solidjs/start/client';
5051

5152
Sentry.init({
5253
dsn: '__PUBLIC_DSN__',
54+
integrations: [solidRouterBrowserTracingIntegration()],
5355
tracesSampleRate: 1.0, // Capture 100% of the transactions
5456
});
5557

@@ -69,7 +71,37 @@ Sentry.init({
6971
});
7072
```
7173

72-
### 4. Run your application
74+
### 4. Server instrumentation
75+
76+
Complete the setup by adding the Sentry middlware to your `src/middleware.ts` file:
77+
78+
```typescript
79+
import { sentryBeforeResponseMiddleware } from '@sentry/solidstart/middleware';
80+
import { createMiddleware } from '@solidjs/start/middleware';
81+
82+
export default createMiddleware({
83+
onBeforeResponse: [
84+
sentryBeforeResponseMiddleware(),
85+
// Add your other middleware handlers after `sentryBeforeResponseMiddleware`
86+
],
87+
});
88+
```
89+
90+
And don't forget to specify `./src/middleware.ts` in your `app.config.ts`:
91+
92+
```typescript
93+
import { defineConfig } from '@solidjs/start/config';
94+
95+
export default defineConfig({
96+
// ...
97+
middleware: './src/middleware.ts',
98+
});
99+
```
100+
101+
The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between
102+
the client and server.
103+
104+
### 5. Run your application
73105

74106
Then run your app
75107

packages/solidstart/package.json

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@
3939
"require": "./build/cjs/index.server.js"
4040
}
4141
},
42+
"./middleware": {
43+
"types": "./middleware.d.ts",
44+
"import": {
45+
"types": "./middleware.d.ts",
46+
"default": "./build/esm/middleware.js"
47+
},
48+
"require": {
49+
"types": "./middleware.d.ts",
50+
"default": "./build/cjs/middleware.js"
51+
}
52+
},
4253
"./solidrouter": {
4354
"types": "./solidrouter.d.ts",
4455
"browser": {
@@ -87,15 +98,15 @@
8798
"build": "run-p build:transpile build:types",
8899
"build:dev": "yarn build",
89100
"build:transpile": "rollup -c rollup.npm.config.mjs",
90-
"build:types": "run-s build:types:core build:types:solidrouter",
101+
"build:types": "run-s build:types:core build:types:subexports",
91102
"build:types:core": "tsc -p tsconfig.types.json",
92-
"build:types:solidrouter": "tsc -p tsconfig.solidrouter-types.json",
103+
"build:types:subexports": "tsc -p tsconfig.subexports-types.json",
93104
"build:watch": "run-p build:transpile:watch build:types:watch",
94105
"build:dev:watch": "yarn build:watch",
95106
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
96107
"build:types:watch": "tsc -p tsconfig.types.json --watch",
97108
"build:tarball": "npm pack",
98-
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts",
109+
"circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts && madge --circular src/solidrouter.client.ts && madge --circular src/solidrouter.server.ts && madge --circular src/solidrouter.ts && madge --circular src/middleware.ts",
99110
"clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server",
100111
"fix": "eslint . --format stylish --fix",
101112
"lint": "eslint . --format stylish",

packages/solidstart/rollup.npm.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default makeNPMConfigVariants(
1212
'src/solidrouter.server.ts',
1313
'src/client/solidrouter.ts',
1414
'src/server/solidrouter.ts',
15+
'src/middleware.ts',
1516
],
1617
// prevent this internal code from ending up in our built package (this doesn't happen automatially because
1718
// the name doesn't match an SDK dependency)

packages/solidstart/src/middleware.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { getTraceData } from '@sentry/core';
2+
import { addNonEnumerableProperty } from '@sentry/utils';
3+
import type { ResponseMiddleware } from '@solidjs/start/middleware';
4+
import type { FetchEvent } from '@solidjs/start/server';
5+
6+
export type ResponseMiddlewareResponse = Parameters<ResponseMiddleware>[1] & {
7+
__sentry_wrapped__?: boolean;
8+
};
9+
10+
function addMetaTagToHead(html: string): string {
11+
const { 'sentry-trace': sentryTrace, baggage } = getTraceData();
12+
13+
if (!sentryTrace) {
14+
return html;
15+
}
16+
17+
const metaTags = [`<meta name="sentry-trace" content="${sentryTrace}">`];
18+
19+
if (baggage) {
20+
metaTags.push(`<meta name="baggage" content="${baggage}">`);
21+
}
22+
23+
const content = `<head>\n${metaTags.join('\n')}\n`;
24+
return html.replace('<head>', content);
25+
}
26+
27+
/**
28+
* Returns an `onBeforeResponse` solid start middleware handler that adds tracing data as
29+
* <meta> tags to a page on pageload to enable distributed tracing.
30+
*/
31+
export function sentryBeforeResponseMiddleware() {
32+
return async function onBeforeResponse(event: FetchEvent, response: ResponseMiddlewareResponse) {
33+
if (!response.body || response.__sentry_wrapped__) {
34+
return;
35+
}
36+
37+
// Ensure we don't double-wrap, in case a user has added the middleware twice
38+
// e.g. once manually, once via the wizard
39+
addNonEnumerableProperty(response, '__sentry_wrapped__', true);
40+
41+
const contentType = event.response.headers.get('content-type');
42+
const isPageloadRequest = contentType && contentType.startsWith('text/html');
43+
44+
if (!isPageloadRequest) {
45+
return;
46+
}
47+
48+
const body = response.body as NodeJS.ReadableStream;
49+
const decoder = new TextDecoder();
50+
response.body = new ReadableStream({
51+
start: async controller => {
52+
for await (const chunk of body) {
53+
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
54+
const modifiedHtml = addMetaTagToHead(html);
55+
controller.enqueue(new TextEncoder().encode(modifiedHtml));
56+
}
57+
controller.close();
58+
},
59+
});
60+
};
61+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import * as SentryCore from '@sentry/core';
2+
import { beforeEach, describe, it, vi } from 'vitest';
3+
import { sentryBeforeResponseMiddleware } from '../src/middleware';
4+
import type { ResponseMiddlewareResponse } from '../src/middleware';
5+
6+
describe('middleware', () => {
7+
describe('sentryBeforeResponseMiddleware', () => {
8+
vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({
9+
'sentry-trace': '123',
10+
baggage: 'abc',
11+
});
12+
13+
const mockFetchEvent = {
14+
request: {},
15+
locals: {},
16+
response: {
17+
// mocks a pageload
18+
headers: new Headers([['content-type', 'text/html']]),
19+
},
20+
nativeEvent: {},
21+
};
22+
23+
let mockMiddlewareHTMLResponse: ResponseMiddlewareResponse;
24+
let mockMiddlewareHTMLNoHeadResponse: ResponseMiddlewareResponse;
25+
let mockMiddlewareJSONResponse: ResponseMiddlewareResponse;
26+
27+
beforeEach(() => {
28+
// h3 doesn't pass a proper Response object to the middleware
29+
mockMiddlewareHTMLResponse = {
30+
body: new Response('<head><meta charset="utf-8"></head>').body,
31+
};
32+
mockMiddlewareHTMLNoHeadResponse = {
33+
body: new Response('<body>Hello World</body>').body,
34+
};
35+
mockMiddlewareJSONResponse = {
36+
body: new Response('{"prefecture": "Kagoshima"}').body,
37+
};
38+
});
39+
40+
it('injects tracing meta tags into the response body', async () => {
41+
const onBeforeResponse = sentryBeforeResponseMiddleware();
42+
onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLResponse);
43+
44+
// for testing convenience, we pass the body back into a proper response
45+
// mockMiddlewareHTMLResponse has been modified by our middleware
46+
const html = await new Response(mockMiddlewareHTMLResponse.body).text();
47+
expect(html).toContain('<meta charset="utf-8">');
48+
expect(html).toContain('<meta name="sentry-trace" content="123">');
49+
expect(html).toContain('<meta name="baggage" content="abc">');
50+
});
51+
52+
it('does not add meta tags if there is no head tag', async () => {
53+
const onBeforeResponse = sentryBeforeResponseMiddleware();
54+
onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLNoHeadResponse);
55+
56+
const html = await new Response(mockMiddlewareHTMLNoHeadResponse.body).text();
57+
expect(html).toEqual('<body>Hello World</body>');
58+
});
59+
60+
it('does not add tracing meta tags twice into the same response', async () => {
61+
const onBeforeResponse1 = sentryBeforeResponseMiddleware();
62+
onBeforeResponse1(mockFetchEvent, mockMiddlewareHTMLResponse);
63+
64+
const onBeforeResponse2 = sentryBeforeResponseMiddleware();
65+
onBeforeResponse2(mockFetchEvent, mockMiddlewareHTMLResponse);
66+
67+
const html = await new Response(mockMiddlewareHTMLResponse.body).text();
68+
expect(html.match(/<meta name="sentry-trace" content="123">/g)).toHaveLength(1);
69+
expect(html.match(/<meta name="baggage" content="abc">/g)).toHaveLength(1);
70+
});
71+
72+
it('does not modify a non-HTML response', async () => {
73+
const onBeforeResponse = sentryBeforeResponseMiddleware();
74+
onBeforeResponse({ ...mockFetchEvent, response: { headers: new Headers() } }, mockMiddlewareJSONResponse);
75+
76+
const json = await new Response(mockMiddlewareJSONResponse.body).json();
77+
expect(json).toEqual({
78+
prefecture: 'Kagoshima',
79+
});
80+
});
81+
});
82+
});

packages/solidstart/tsconfig.solidrouter-types.json renamed to packages/solidstart/tsconfig.subexports-types.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"src/solidrouter.server.ts",
1616
"src/server/solidrouter.ts",
1717
"src/solidrouter.ts",
18+
"src/middleware.ts",
1819
],
1920
// Without this, we cannot output into the root dir
2021
"exclude": []

packages/solidstart/tsconfig.types.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"src/client/solidrouter.ts",
1515
"src/solidrouter.server.ts",
1616
"src/server/solidrouter.ts",
17-
"src/solidrouter.ts"
17+
"src/solidrouter.ts",
18+
"src/middleware.ts",
1819
]
1920
}

0 commit comments

Comments
 (0)