Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(solidstart): Add sentry onBeforeResponse middleware to enable distributed tracing #13221

Merged
merged 2 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
34 changes: 33 additions & 1 deletion packages/solidstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ Initialize the SDK in `entry-client.jsx`

```jsx
import * as Sentry from '@sentry/solidstart';
import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter';
import { mount, StartClient } from '@solidjs/start/client';

Sentry.init({
dsn: '__PUBLIC_DSN__',
integrations: [solidRouterBrowserTracingIntegration()],
tracesSampleRate: 1.0, // Capture 100% of the transactions
});

Expand All @@ -69,7 +71,37 @@ Sentry.init({
});
```

### 4. Run your application
### 4. Server instrumentation

Complete the setup 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 don't forget to specify `./src/middleware.ts` in your `app.config.ts`
andreiborza marked this conversation as resolved.
Show resolved Hide resolved

```typescript
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
// ...
middleware: './src/middleware.ts',
});
```

The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between
the client and server.

### 5. Run your application

Then run your app

Expand Down
17 changes: 14 additions & 3 deletions packages/solidstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@
"require": "./build/cjs/index.server.js"
}
},
"./middleware": {
"types": "./middleware.d.ts",
"import": {
"types": "./middleware.d.ts",
"default": "./build/esm/middleware.js"
},
"require": {
"types": "./middleware.d.ts",
"default": "./build/cjs/middleware.js"
}
},
"./solidrouter": {
"types": "./solidrouter.d.ts",
"browser": {
Expand Down Expand Up @@ -87,15 +98,15 @@
"build": "run-p build:transpile build:types",
"build:dev": "yarn build",
"build:transpile": "rollup -c rollup.npm.config.mjs",
"build:types": "run-s build:types:core build:types:solidrouter",
"build:types": "run-s build:types:core build:types:subexports",
"build:types:core": "tsc -p tsconfig.types.json",
"build:types:solidrouter": "tsc -p tsconfig.solidrouter-types.json",
"build:types:subexports": "tsc -p tsconfig.subexports-types.json",
"build:watch": "run-p build:transpile:watch build:types:watch",
"build:dev:watch": "yarn build:watch",
"build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
"build:types:watch": "tsc -p tsconfig.types.json --watch",
"build:tarball": "npm pack",
"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",
"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",
"clean": "rimraf build coverage sentry-solidstart-*.tgz ./*.d.ts ./*.d.ts.map ./client ./server",
"fix": "eslint . --format stylish --fix",
"lint": "eslint . --format stylish",
Expand Down
1 change: 1 addition & 0 deletions packages/solidstart/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default makeNPMConfigVariants(
'src/solidrouter.server.ts',
'src/client/solidrouter.ts',
'src/server/solidrouter.ts',
'src/middleware.ts',
],
// prevent this internal code from ending up in our built package (this doesn't happen automatially because
// the name doesn't match an SDK dependency)
Expand Down
61 changes: 61 additions & 0 deletions packages/solidstart/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { getTraceData } from '@sentry/core';
import { addNonEnumerableProperty } from '@sentry/utils';
import type { ResponseMiddleware } from '@solidjs/start/middleware';
import type { FetchEvent } from '@solidjs/start/server';

export type ResponseMiddlewareResponse = Parameters<ResponseMiddleware>[1] & {
__sentry_wrapped__?: boolean;
};

function addMetaTagToHead(html: string): string {
const { 'sentry-trace': sentryTrace, baggage } = getTraceData();

if (!sentryTrace) {
return html;
}

const metaTags = [`<meta name="sentry-trace" content="${sentryTrace}">`];

if (baggage) {
metaTags.push(`<meta name="baggage" content="${baggage}">`);
}

const content = `<head>\n${metaTags.join('\n')}\n`;
return html.replace('<head>', content);
}

/**
* Returns an `onBeforeResponse` solid start middleware handler that adds tracing data as
* <meta> tags to a page on pageload to enable distributed tracing.
*/
export function sentryBeforeResponseMiddleware() {
return async function onBeforeResponse(event: FetchEvent, response: ResponseMiddlewareResponse) {
if (!response.body || response.__sentry_wrapped__) {
return;
}

// Ensure we don't double-wrap, in case a user has added the middleware twice
// e.g. once manually, once via the wizard
addNonEnumerableProperty(response, '__sentry_wrapped__', true);

const contentType = event.response.headers.get('content-type');
const isPageloadRequest = contentType && contentType.startsWith('text/html');

if (!isPageloadRequest) {
return;
}

const body = response.body as NodeJS.ReadableStream;
const decoder = new TextDecoder();
response.body = new ReadableStream({
start: async controller => {
for await (const chunk of body) {
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
const modifiedHtml = addMetaTagToHead(html);
controller.enqueue(new TextEncoder().encode(modifiedHtml));
}
controller.close();
},
});
};
}
82 changes: 82 additions & 0 deletions packages/solidstart/test/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as SentryCore from '@sentry/core';
import { beforeEach, describe, it, vi } from 'vitest';
import { sentryBeforeResponseMiddleware } from '../src/middleware';
import type { ResponseMiddlewareResponse } from '../src/middleware';

describe('middleware', () => {
describe('sentryBeforeResponseMiddleware', () => {
vi.spyOn(SentryCore, 'getTraceData').mockReturnValue({
'sentry-trace': '123',
baggage: 'abc',
});

const mockFetchEvent = {
request: {},
locals: {},
response: {
// mocks a pageload
headers: new Headers([['content-type', 'text/html']]),
},
nativeEvent: {},
};

let mockMiddlewareHTMLResponse: ResponseMiddlewareResponse;
let mockMiddlewareHTMLNoHeadResponse: ResponseMiddlewareResponse;
let mockMiddlewareJSONResponse: ResponseMiddlewareResponse;

beforeEach(() => {
// h3 doesn't pass a proper Response object to the middleware
mockMiddlewareHTMLResponse = {
body: new Response('<head><meta charset="utf-8"></head>').body,
};
mockMiddlewareHTMLNoHeadResponse = {
body: new Response('<body>Hello World</body>').body,
};
mockMiddlewareJSONResponse = {
body: new Response('{"prefecture": "Kagoshima"}').body,
};
});

it('injects tracing meta tags into the response body', async () => {
const onBeforeResponse = sentryBeforeResponseMiddleware();
onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLResponse);

// for testing convenience, we pass the body back into a proper response
// mockMiddlewareHTMLResponse has been modified by our middleware
const html = await new Response(mockMiddlewareHTMLResponse.body).text();
expect(html).toContain('<meta charset="utf-8">');
expect(html).toContain('<meta name="sentry-trace" content="123">');
expect(html).toContain('<meta name="baggage" content="abc">');
});

it('does not add meta tags if there is no head tag', async () => {
const onBeforeResponse = sentryBeforeResponseMiddleware();
onBeforeResponse(mockFetchEvent, mockMiddlewareHTMLNoHeadResponse);

const html = await new Response(mockMiddlewareHTMLNoHeadResponse.body).text();
expect(html).toEqual('<body>Hello World</body>');
});

it('does not add tracing meta tags twice into the same response', async () => {
const onBeforeResponse1 = sentryBeforeResponseMiddleware();
onBeforeResponse1(mockFetchEvent, mockMiddlewareHTMLResponse);

const onBeforeResponse2 = sentryBeforeResponseMiddleware();
onBeforeResponse2(mockFetchEvent, mockMiddlewareHTMLResponse);

const html = await new Response(mockMiddlewareHTMLResponse.body).text();
expect(html.match(/<meta name="sentry-trace" content="123">/g)).toHaveLength(1);
expect(html.match(/<meta name="baggage" content="abc">/g)).toHaveLength(1);
});

it('does not modify a non-HTML response', async () => {
const onBeforeResponse = sentryBeforeResponseMiddleware();
onBeforeResponse({ ...mockFetchEvent, response: { headers: new Headers() } }, mockMiddlewareJSONResponse);

const json = await new Response(mockMiddlewareJSONResponse.body).json();
expect(json).toEqual({
prefecture: 'Kagoshima',
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"src/solidrouter.server.ts",
"src/server/solidrouter.ts",
"src/solidrouter.ts",
"src/middleware.ts",
],
// Without this, we cannot output into the root dir
"exclude": []
Expand Down
3 changes: 2 additions & 1 deletion packages/solidstart/tsconfig.types.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"src/client/solidrouter.ts",
"src/solidrouter.server.ts",
"src/server/solidrouter.ts",
"src/solidrouter.ts"
"src/solidrouter.ts",
"src/middleware.ts",
]
}
Loading