Skip to content

Commit

Permalink
feat(solidstart): Add sentry onBeforeResponse middleware to enable …
Browse files Browse the repository at this point in the history
…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]>
  • Loading branch information
andreiborza and Lms24 committed Aug 5, 2024
1 parent bedc385 commit 4ebac94
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 5 deletions.
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`:

```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",
]
}

0 comments on commit 4ebac94

Please sign in to comment.