From 32a7a04513913947310def2cd02c8a8dfb4fd05e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 9 Nov 2023 14:00:00 +0100 Subject: [PATCH] use ReadableStream to modify html chunks --- packages/astro/src/server/meta.ts | 3 +- packages/astro/src/server/middleware.ts | 42 ++++++++++++++++++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts index abb1536ab608..7257cf01db51 100644 --- a/packages/astro/src/server/meta.ts +++ b/packages/astro/src/server/meta.ts @@ -1,6 +1,5 @@ -import type { Hub } from '@sentry/core'; import { getDynamicSamplingContextFromClient } from '@sentry/core'; -import type { Span } from '@sentry/types'; +import type { Hub, Span } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, generateSentryTraceHeader, diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 999a223262d6..1c2933e1eccc 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,4 +1,5 @@ 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'; @@ -109,16 +110,29 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH return originalResponse; } - const html = await originalResponse.text(); - if (typeof html !== 'string' || !html.includes('')) { + const { body, ...restOfOriginalResponse } = originalResponse; + + // 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 = body as NodeJS.ReadableStream | null; + if (!originalBody) { return originalResponse; } - const { sentryTrace, baggage } = getTracingMetaTags(span, hub); - const content = `\n${sentryTrace}\n${baggage}\n`; - const modifiedHtml = html.replace('', content); - - return new Response(modifiedHtml, 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(modifiedHtml); + } + controller.close(); + }, + }); + + return new Response(newResponseStream, restOfOriginalResponse); }, ); return res; @@ -130,6 +144,20 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH }; }; +/** + * This function optimistically assumes that the HTML coming in chunks will not be split + * within the 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 = `\n${sentryTrace}\n${baggage}\n`; + return htmlChunk.replace('', 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.