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(sveltekit): Add meta tag for backend -> frontend #7574

Merged
merged 1 commit into from
Mar 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 18 additions & 3 deletions packages/sveltekit/src/server/handle.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
import type { Span } from '@sentry/core';
import { trace } from '@sentry/core';
import { getActiveTransaction, trace } from '@sentry/core';
import { captureException } from '@sentry/node';
import {
addExceptionMechanism,
baggageHeaderToDynamicSamplingContext,
dynamicSamplingContextToSentryBaggageHeader,
extractTraceparentData,
objectify,
} from '@sentry/utils';
import type { Handle } from '@sveltejs/kit';
import type { Handle, ResolveOptions } from '@sveltejs/kit';
import * as domain from 'domain';

function sendErrorToSentry(e: unknown): unknown {
Expand All @@ -34,6 +35,20 @@ function sendErrorToSentry(e: unknown): unknown {
return objectifiedErr;
}

export const transformPageChunk: NonNullable<ResolveOptions['transformPageChunk']> = ({ html }) => {
const transaction = getActiveTransaction();
if (transaction) {
const traceparentData = transaction.toTraceparent();
const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader(transaction.getDynamicSamplingContext());
const content = `<meta name="sentry-trace" content="${traceparentData}"/>
<meta name="baggage" content="${dynamicSamplingContext}"/>
%sveltekit.head%`;
return html.replace('%sveltekit.head%', content);
}

return html;
};

/**
* A SvelteKit handle function that wraps the request for Sentry error and
* performance monitoring.
Expand Down Expand Up @@ -68,7 +83,7 @@ export const sentryHandle: Handle = ({ event, resolve }) => {
},
},
async (span?: Span) => {
const res = await resolve(event);
const res = await resolve(event, { transformPageChunk });
if (span) {
span.setHttpStatus(res.status);
}
Expand Down
71 changes: 57 additions & 14 deletions packages/sveltekit/test/server/handle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Transaction } from '@sentry/types';
import type { Handle } from '@sveltejs/kit';
import { vi } from 'vitest';

import { sentryHandle } from '../../src/server/handle';
import { sentryHandle, transformPageChunk } from '../../src/server/handle';
import { getDefaultNodeClientOptions } from '../utils';

const mockCaptureException = vi.fn();
Expand Down Expand Up @@ -94,22 +94,22 @@ function resolve(type: Type, isError: boolean): Parameters<Handle>[0]['resolve']
let hub: Hub;
let client: NodeClient;

describe('handleSentry', () => {
beforeAll(() => {
addTracingExtensions();
});
beforeAll(() => {
addTracingExtensions();
});

beforeEach(() => {
mockScope = new Scope();
const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 });
client = new NodeClient(options);
hub = new Hub(client);
makeMain(hub);
beforeEach(() => {
mockScope = new Scope();
const options = getDefaultNodeClientOptions({ tracesSampleRate: 1.0 });
client = new NodeClient(options);
hub = new Hub(client);
makeMain(hub);

mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
});
mockCaptureException.mockClear();
mockAddExceptionMechanism.mockClear();
});

describe('handleSentry', () => {
describe.each([
// isSync, isError, expectedResponse
[Type.Sync, true, undefined],
Expand Down Expand Up @@ -247,5 +247,48 @@ describe('handleSentry', () => {
);
}
});

it('calls `transformPageChunk`', async () => {
const mockResolve = vi.fn().mockImplementation(resolve(type, isError));
const event = mockEvent();
try {
await sentryHandle({ event, resolve: mockResolve });
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect(e.message).toEqual(type);
}

expect(mockResolve).toHaveBeenCalledTimes(1);
expect(mockResolve).toHaveBeenCalledWith(event, { transformPageChunk: expect.any(Function) });
});
});
});

describe('transformPageChunk', () => {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>`;

it('does not add meta tags if no active transaction', () => {
const transformed = transformPageChunk({ html, done: true });
expect(transformed).toEqual(html);
});

it('adds meta tags if there is an active transaction', () => {
const transaction = hub.startTransaction({ name: 'test' });
hub.getScope().setSpan(transaction);
const transformed = transformPageChunk({ html, done: true }) as string;

expect(transformed.includes('<meta name="sentry-trace"')).toEqual(true);
expect(transformed.includes('<meta name="baggage"')).toEqual(true);
});
});