Skip to content

Commit

Permalink
Merge pull request #9510 from getsentry/prepare-release/7.80.0
Browse files Browse the repository at this point in the history
meta: Update changelog for 7.80.0
  • Loading branch information
Lms24 authored Nov 9, 2023
2 parents b435ea5 + e0a9d58 commit 2789420
Show file tree
Hide file tree
Showing 57 changed files with 744 additions and 205 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

## 7.80.0

- feat(astro): Add distributed tracing via `<meta>` tags (#9483)
- feat(node): Capture internal server errors in trpc middleware (#9482)
- feat(remix): Export a type to use for `MetaFunction` parameters (#9493)
- fix(astro): Mark SDK package as Astro-external (#9509)
- ref(nextjs): Don't initialize Server SDK during build (#9503)

## 7.79.0

- feat(tracing): Add span `origin` to trace context (#9472)
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ import { sequence } from "astro:middleware";
import * as Sentry from "@sentry/astro";

export const onRequest = sequence(
Sentry.sentryMiddleware(),
// Add your other handlers after sentryMiddleware
Sentry.handleRequest(),
// Add your other handlers after Sentry.handleRequest()
);
```

Expand Down
3 changes: 3 additions & 0 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,8 @@
},
"volta": {
"extends": "../../package.json"
},
"astro": {
"external": true
}
}
78 changes: 78 additions & 0 deletions packages/astro/src/server/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { getDynamicSamplingContextFromClient } from '@sentry/core';
import type { Hub, Span } from '@sentry/types';
import {
dynamicSamplingContextToSentryBaggageHeader,
generateSentryTraceHeader,
logger,
TRACEPARENT_REGEXP,
} from '@sentry/utils';

/**
* Extracts the tracing data from the current span or from the client's scope
* (via transaction or propagation context) and renders the data to <meta> tags.
*
* This function creates two serialized <meta> tags:
* - `<meta name="sentry-trace" content="..."/>`
* - `<meta name="baggage" content="..."/>`
*
* TODO: Extract this later on and export it from the Core or Node SDK
*
* @param span the currently active span
* @param client the SDK's client
*
* @returns an object with the two serialized <meta> tags
*/
export function getTracingMetaTags(span: Span | undefined, hub: Hub): { sentryTrace: string; baggage?: string } {
const scope = hub.getScope();
const client = hub.getClient();
const { dsc, sampled, traceId } = scope.getPropagationContext();
const transaction = span?.transaction;

const sentryTrace = span ? span.toTraceparent() : generateSentryTraceHeader(traceId, undefined, sampled);

const dynamicSamplingContext = transaction
? transaction.getDynamicSamplingContext()
: dsc
? dsc
: client
? getDynamicSamplingContextFromClient(traceId, client, scope)
: undefined;

const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);

const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace);
if (!isValidSentryTraceHeader) {
logger.warn('Invalid sentry-trace data. Returning empty <meta name="sentry-trace"/> tag');
}

const validBaggage = isValidBaggageString(baggage);
if (!validBaggage) {
logger.warn('Invalid baggage data. Returning empty <meta name="baggage"/> tag');
}

return {
sentryTrace: `<meta name="sentry-trace" content="${isValidSentryTraceHeader ? sentryTrace : ''}"/>`,
baggage: baggage && `<meta name="baggage" content="${validBaggage ? baggage : ''}"/>`,
};
}

/**
* Tests string against baggage spec as defined in:
*
* - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition
* - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
*
* exported for testing
*/
export function isValidBaggageString(baggage?: string): boolean {
if (!baggage || !baggage.length) {
return false;
}
const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
const spaces = '\\s*';
const baggageRegex = new RegExp(
`^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
);
return baggageRegex.test(baggage);
}
66 changes: 59 additions & 7 deletions packages/astro/src/server/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { captureException, configureScope, startSpan } from '@sentry/node';
import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node';
import type { Hub, Span } from '@sentry/types';
import { addExceptionMechanism, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils';
import type { APIContext, MiddlewareResponseHandler } from 'astro';

import { getTracingMetaTags } from './meta';

type MiddlewareOptions = {
/**
* If true, the client IP will be attached to the event by calling `setUser`.
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
* This will only work if your app is configured for SSR
* Important: Only enable this option if your Astro app is configured for (hybrid) SSR
* via the `output: 'server' | 'hybrid'` option in your `astro.config.mjs` file.
* Otherwise, Astro will throw an error when starting the server.
*
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
* @default false (recommended)
*/
trackClientIp?: boolean;

/**
* If true, the headers from the request will be attached to the event by calling `setExtra`.
*
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
*
* @default false (recommended)
Expand Down Expand Up @@ -93,11 +100,42 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
},
},
async span => {
const res = await next();
if (span && res.status) {
span.setHttpStatus(res.status);
const originalResponse = await next();

if (span && originalResponse.status) {
span.setHttpStatus(originalResponse.status);
}

const hub = getCurrentHub();
const client = hub.getClient();
const contentType = originalResponse.headers.get('content-type');

const isPageloadRequest = contentType && contentType.startsWith('text/html');
if (!isPageloadRequest || !client) {
return originalResponse;
}
return res;

// Type case necessary b/c the body's ReadableStream type doesn't include
// the async iterator that is actually available in Node
// We later on use the async iterator to read the body chunks
// see https://github.com/microsoft/TypeScript/issues/39051
const originalBody = originalResponse.body as NodeJS.ReadableStream | null;
if (!originalBody) {
return originalResponse;
}

const newResponseStream = new ReadableStream({
start: async controller => {
for await (const chunk of originalBody) {
const html = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
const modifiedHtml = addMetaTagToHead(html, hub, span);
controller.enqueue(new TextEncoder().encode(modifiedHtml));
}
controller.close();
},
});

return new Response(newResponseStream, originalResponse);
},
);
return res;
Expand All @@ -109,6 +147,20 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
};
};

/**
* This function optimistically assumes that the HTML coming in chunks will not be split
* within the <head> tag. If this still happens, we simply won't replace anything.
*/
function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string {
if (typeof htmlChunk !== 'string') {
return htmlChunk;
}

const { sentryTrace, baggage } = getTracingMetaTags(span, hub);
const content = `<head>\n${sentryTrace}\n${baggage}\n`;
return htmlChunk.replace('<head>', content);
}

/**
* Interpolates the route from the URL and the passed params.
* Best we can do to get a route name instead of a raw URL.
Expand Down
178 changes: 178 additions & 0 deletions packages/astro/test/server/meta.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import * as SentryCore from '@sentry/core';
import { vi } from 'vitest';

import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta';

const mockedSpan = {
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
transaction: {
getDynamicSamplingContext: () => ({
environment: 'production',
}),
},
};

const mockedHub = {
getScope: () => ({
getPropagationContext: () => ({
traceId: '123',
}),
}),
getClient: () => ({}),
};

describe('getTracingMetaTags', () => {
it('returns the tracing tags from the span, if it is provided', () => {
{
// @ts-expect-error - only passing a partial span object
const tags = getTracingMetaTags(mockedSpan, mockedHub);

expect(tags).toEqual({
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
baggage: '<meta name="baggage" content="sentry-environment=production"/>',
});
}
});

it('returns propagationContext DSC data if no span is available', () => {
const tags = getTracingMetaTags(undefined, {
...mockedHub,
// @ts-expect-error - only passing a partial scope object
getScope: () => ({
getPropagationContext: () => ({
traceId: '12345678901234567890123456789012',
sampled: true,
spanId: '1234567890123456',
dsc: {
environment: 'staging',
public_key: 'key',
trace_id: '12345678901234567890123456789012',
},
}),
}),
});

expect(tags).toEqual({
sentryTrace: expect.stringMatching(
/<meta name="sentry-trace" content="12345678901234567890123456789012-(.{16})-1"\/>/,
),
baggage:
'<meta name="baggage" content="sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012"/>',
});
});

it('returns only the `sentry-trace` tag if no DSC is available', () => {
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
trace_id: '',
public_key: undefined,
});

const tags = getTracingMetaTags(
// @ts-expect-error - only passing a partial span object
{
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
transaction: undefined,
},
mockedHub,
);

expect(tags).toEqual({
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
});
});

it('returns only the `sentry-trace` tag if no DSC is available', () => {
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
trace_id: '',
public_key: undefined,
});

const tags = getTracingMetaTags(
// @ts-expect-error - only passing a partial span object
{
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
transaction: undefined,
},
{
...mockedHub,
getClient: () => undefined,
},
);

expect(tags).toEqual({
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
});
});
});

describe('isValidBaggageString', () => {
it.each([
'sentry-environment=production',
'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc',
// @ is allowed in values
'[email protected]',
// spaces are allowed around the delimiters
'sentry-environment=staging , sentry-public_key=key ,[email protected]',
'sentry-environment=staging , thirdparty=value ,[email protected]',
// these characters are explicitly allowed for keys in the baggage spec:
"!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true",
// special characters in values are fine (except for ",;\ - see other test)
'key=(value)',
'key=[{(value)}]',
'key=some$value',
'key=more#value',
'key=max&value',
'key=max:value',
'key=x=value',
])('returns true if the baggage string is valid (%s)', baggageString => {
expect(isValidBaggageString(baggageString)).toBe(true);
});

it.each([
// baggage spec doesn't permit leading spaces
' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc',
// no spaces in keys or values
'sentry-public key=key',
'sentry-publickey=my key',
// no delimiters ("(),/:;<=>?@[\]{}") in keys
'asdf(x=value',
'asdf)x=value',
'asdf,x=value',
'asdf/x=value',
'asdf:x=value',
'asdf;x=value',
'asdf<x=value',
'asdf>x=value',
'asdf?x=value',
'asdf@x=value',
'asdf[x=value',
'asdf]x=value',
'asdf\\x=value',
'asdf{x=value',
'asdf}x=value',
// no ,;\" in values
'key=va,lue',
'key=va;lue',
'key=va\\lue',
'key=va"lue"',
// baggage headers can have properties but we currently don't support them
'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value',
// no fishy stuff
'absolutely not a valid baggage string',
'val"/><script>alert("xss")</script>',
'something"/>',
'<script>alert("xss")</script>',
'/>',
'" onblur="alert("xss")',
])('returns false if the baggage string is invalid (%s)', baggageString => {
expect(isValidBaggageString(baggageString)).toBe(false);
});

it('returns false if the baggage string is empty', () => {
expect(isValidBaggageString('')).toBe(false);
});

it('returns false if the baggage string is empty', () => {
expect(isValidBaggageString(undefined)).toBe(false);
});
});
Loading

0 comments on commit 2789420

Please sign in to comment.