From 667a4fad525560d41ae4a7efcb68e68567afe4c8 Mon Sep 17 00:00:00 2001 From: Jorge Azevedo Date: Fri, 17 Jul 2026 14:07:07 +0100 Subject: [PATCH 1/5] First pass at tracing --- docker-compose.yaml | 15 +- .../__snapshots__/renderingStack.test.ts.snap | 129 +++++++++++++ dotcom-rendering/cdk/lib/renderingStack.ts | 27 +++ dotcom-rendering/package.json | 5 + .../src/server/handler.front.web.ts | 151 +++++++++------ dotcom-rendering/src/server/lib/tracing.ts | 119 ++++++++++++ .../src/server/render.front.web.tsx | 159 ++++++++------- dotcom-rendering/src/server/server.prod.ts | 17 +- dotcom-rendering/src/server/server.ts | 2 + pnpm-lock.yaml | 181 +++++++++++++++++- 10 files changed, 670 insertions(+), 135 deletions(-) create mode 100644 dotcom-rendering/src/server/lib/tracing.ts diff --git a/docker-compose.yaml b/docker-compose.yaml index 3fd54642df3..430ed0c3a70 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,6 +16,10 @@ services: # Explicitly tell AWS SDK where to find credentials AWS_SHARED_CREDENTIALS_FILE: /.aws/credentials + + # OpenTelemetry exporter endpoint (Jaeger) + OTEL_EXPORTER_OTLP_ENDPOINT: http://jaeger:4318 + OTEL_SERVICE_NAME: tag-page-rendering ports: - '9000:9000' @@ -48,6 +52,13 @@ services: depends_on: tag-page-rendering: condition: service_healthy + volumes: + - ./tag-page-payload.json:/data/tag-page-payload.json:ro command: | - curl "https://www.theguardian.com/tone/minutebyminute.json?dcr=true" --silent > data.json && \ - curl -X POST http://localhost:9000/TagPage -d @data.json -H "Content-Type: application/json" + curl -X POST http://tag-page-rendering:9000/TagPage -d @/data/tag-page-payload.json -H "Content-Type: application/json" + + jaeger: + image: jaegertracing/jaeger:2.6.0 + ports: + - '16686:16686' + - '4318:4318' diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index a9a008a4a10..291a8c367dc 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -8,6 +8,7 @@ exports[`The RenderingCDKStack matches the snapshot 1`] = ` "GuAllowPolicy", "GuAllowPolicy", "GuAllowPolicy", + "GuAllowPolicy", "GuVpcParameter", "GuSubnetListParameter", "GuSubnetListParameter", @@ -190,6 +191,32 @@ exports[`The RenderingCDKStack matches the snapshot 1`] = ` }, "Type": "AWS::IAM::Policy", }, + "AllowPolicyXRay6A9BDA0E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyXRay6A9BDA0E", + "Roles": [ + { + "Ref": "InstanceRoleArticlerendering0D4717E8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "AutoScalingGroupArticlerenderingASG8488C3F1": { "Properties": { "HealthCheckGracePeriod": 120, @@ -1623,6 +1650,7 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "GuAllowPolicy", "GuAllowPolicy", "GuAllowPolicy", + "GuAllowPolicy", "GuVpcParameter", "GuSubnetListParameter", "GuSubnetListParameter", @@ -1823,6 +1851,35 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, "Type": "AWS::IAM::Policy", }, + "AllowPolicyXRay6A9BDA0E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "xray:PutTraceSegments", + "xray:PutTelemetryRecords", + "xray:GetSamplingRules", + "xray:GetSamplingTargets", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyXRay6A9BDA0E", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "AutoScalingGroupTagpagerenderingASG7F7E0748": { "Properties": { "HealthCheckGracePeriod": 120, @@ -2157,6 +2214,14 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "Name": "GU_STACK", "Value": "frontend", }, + { + "Name": "OTEL_EXPORTER_OTLP_ENDPOINT", + "Value": "http://localhost:4318", + }, + { + "Name": "OTEL_SERVICE_NAME", + "Value": "tag-page-rendering", + }, ], "Essential": true, "Image": { @@ -2244,6 +2309,32 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "ReadonlyRootFilesystem": true, "VersionConsistency": "disabled", }, + { + "Command": [ + "--config=/etc/ecs/ecs-default-config.yaml", + ], + "Cpu": 256, + "Essential": true, + "Image": "public.ecr.aws/aws-observability/aws-otel-collector:latest", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "EcsTaskDefinitionadotcollectorLogGroup9B99E580", + }, + "awslogs-region": "eu-west-1", + "awslogs-stream-prefix": "tag-page-rendering-adot", + }, + }, + "MemoryReservation": 256, + "Name": "adot-collector", + "PortMappings": [ + { + "ContainerPort": 4318, + "Protocol": "tcp", + }, + ], + }, ], "Cpu": "1024", "ExecutionRoleArn": { @@ -2380,6 +2471,19 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE ], }, }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsTaskDefinitionadotcollectorLogGroup9B99E580", + "Arn", + ], + }, + }, ], "Version": "2012-10-17", }, @@ -2453,6 +2557,31 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE }, "Type": "AWS::IAM::Role", }, + "EcsTaskDefinitionadotcollectorLogGroup9B99E580": { + "DeletionPolicy": "Retain", + "Properties": { + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, "GetDistributablePolicyTagpagerendering346128E3": { "Properties": { "PolicyDocument": { diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index ce5bbb6cc3c..5a33e13e5bb 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -18,6 +18,7 @@ import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'; import type { InstanceType } from 'aws-cdk-lib/aws-ec2'; import { Peer } from 'aws-cdk-lib/aws-ec2'; import { ClusterSettings } from 'aws-cdk-lib/aws-ecs/mixins'; +import { ContainerImage, LogDrivers } from 'aws-cdk-lib/aws-ecs'; import { Subscription, SubscriptionProtocol, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { getUserData } from './userData'; @@ -239,6 +240,15 @@ export class RenderingCDKStack extends CDKStack { actions: ['cloudwatch:*', 'logs:*'], resources: ['*'], }), + new GuAllowPolicy(this, 'AllowPolicyXRay', { + actions: [ + 'xray:PutTraceSegments', + 'xray:PutTelemetryRecords', + 'xray:GetSamplingRules', + 'xray:GetSamplingTargets', + ], + resources: ['*'], + }), new GuAllowPolicy(this, 'AllowPolicyDescribeDecryptKms', { actions: ['kms:Decrypt', 'kms:DescribeKey'], resources: [ @@ -302,6 +312,8 @@ export class RenderingCDKStack extends CDKStack { GU_STAGE: stage, GU_APP: guApp, GU_STACK: guStack, + OTEL_EXPORTER_OTLP_ENDPOINT: 'http://localhost:4318', + OTEL_SERVICE_NAME: guApp, }; for (const [key, value] of Object.entries(ecsEnvVars)) { @@ -317,6 +329,21 @@ export class RenderingCDKStack extends CDKStack { { name: 'containerInsights', value: 'enhanced' }, ]), ); + + // ADOT (AWS Distro for OpenTelemetry) sidecar to receive OTLP + // traces from the app and forward them to AWS X-Ray. + app.ecsService?.taskDefinition.addContainer('adot-collector', { + image: ContainerImage.fromRegistry( + 'public.ecr.aws/aws-observability/aws-otel-collector:latest', + ), + command: ['--config=/etc/ecs/ecs-default-config.yaml'], + logging: LogDrivers.awsLogs({ + streamPrefix: `${guApp}-adot`, + }), + portMappings: [{ containerPort: 4318 }], + cpu: 256, + memoryReservationMiB: 256, + }); } /** diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 9ac77be901b..021f4a1b934 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -46,6 +46,11 @@ "@guardian/source-development-kitchen": "28.1.0", "@guardian/support-dotcom-components": "10.1.0", "@guardian/tsconfig": "catalog:", + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/context-async-hooks": "^2.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.9.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", "@playwright/test": "1.60.0", "@sentry/browser": "10.52.0", "@storybook/addon-a11y": "10.3.3", diff --git a/dotcom-rendering/src/server/handler.front.web.ts b/dotcom-rendering/src/server/handler.front.web.ts index 6e94ff02f33..acaf291fe11 100644 --- a/dotcom-rendering/src/server/handler.front.web.ts +++ b/dotcom-rendering/src/server/handler.front.web.ts @@ -17,6 +17,7 @@ import type { Front } from '../types/front'; import type { FETagType } from '../types/tag'; import type { TagPage } from '../types/tagPage'; import { makePrefetchHeader } from './lib/header'; +import { traceSync } from './lib/tracing'; import { renderFront, renderTagPage } from './render.front.web'; const enhanceFront = (body: unknown): Front => { @@ -106,57 +107,79 @@ const headerImage = (tags: FETagType[]): TagPage['header']['image'] => { }; const enhanceTagPage = (body: unknown): TagPage => { - const data: FETagPage = validateAsFETagPage(body); + return traceSync('enhanceTagPage', (parentSpan) => { + const data: FETagPage = traceSync('validateAsFETagPage', () => + validateAsFETagPage(body), + ); + + parentSpan.setAttribute('tagPage.pageId', data.pageId); + parentSpan.setAttribute('tagPage.editionId', data.editionId); + parentSpan.setAttribute('tagPage.contentsCount', data.contents.length); + + const branding = data.commercialProperties[data.editionId].branding; + + const tagPageBranding = branding + ? decideTagPageBranding({ + branding, + }) + : undefined; + + const enhancedCards = traceSync('enhanceCards', (span) => { + const cards = enhanceCards(data.contents, { + cardInTagPage: true, + pageId: data.pageId, + discussionApiUrl: data.config.discussionApiUrl, + editionId: data.editionId, + stripBranding: !!tagPageBranding, + }); + span.setAttribute('cards.count', cards.length); + return cards; + }); + + const speed = traceSync('getSpeedFromTrails', () => + getSpeedFromTrails(data.contents), + ); + + const groupedTrails = traceSync('groupTrailsByDates', (span) => { + const groups = groupTrailsByDates( + enhancedCards, + data.editionId, + speed === 'slow' || data.forceDay, + ); + span.setAttribute('groups.count', groups.length); + return groups; + }); + + const trendingTopics = traceSync('extractTrendingTopics', () => + extractTrendingTopics(data.contents, data.pageId), + ); - const branding = data.commercialProperties[data.editionId].branding; - - const tagPageBranding = branding - ? decideTagPageBranding({ - branding, - }) - : undefined; - - const enhancedCards = enhanceCards(data.contents, { - cardInTagPage: true, - pageId: data.pageId, - discussionApiUrl: data.config.discussionApiUrl, - editionId: data.editionId, - stripBranding: !!tagPageBranding, + return { + ...data, + webTitle: tagPageWebTitle(data), + tags: data.tags.tags, + groupedTrails, + speed, + pagination: + data.pagination && data.pagination.lastPage > 1 + ? { + ...data.pagination, + sectionName: data.webTitle, + pageId: data.pageId, + } + : undefined, + trendingTopics, + header: { + title: data.webTitle, + description: + data.tags.tags[0]?.properties.bio ?? + data.tags.tags[0]?.properties.description, + image: headerImage(data.tags.tags), + }, + branding: tagPageBranding, + canonicalUrl: data.canonicalUrl, + }; }); - - const speed = getSpeedFromTrails(data.contents); - - const groupedTrails = groupTrailsByDates( - enhancedCards, - data.editionId, - speed === 'slow' || data.forceDay, - ); - - return { - ...data, - webTitle: tagPageWebTitle(data), - tags: data.tags.tags, - groupedTrails, - speed, - pagination: - data.pagination && data.pagination.lastPage > 1 - ? { - ...data.pagination, - sectionName: data.webTitle, - pageId: data.pageId, - } - : undefined, - trendingTopics: extractTrendingTopics(data.contents, data.pageId), - header: { - title: data.webTitle, - description: - data.tags.tags[0]?.properties.bio ?? - data.tags.tags[0]?.properties.description, - image: headerImage(data.tags.tags), - }, - branding: tagPageBranding, - canonicalUrl: data.canonicalUrl, - }; }; export const handleFront: RequestHandler = ({ body }, res) => { @@ -167,10 +190,30 @@ export const handleFront: RequestHandler = ({ body }, res) => { res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; -export const handleTagPage: RequestHandler = ({ body }, res) => { - const tagPage = enhanceTagPage(body); - const { html, prefetchScripts } = renderTagPage({ - tagPage, +export const handleTagPage: RequestHandler = (req, res) => { + traceSync('handleTagPage', (span) => { + const requestId = req.headers['x-request-id']; + if (requestId) { + span.setAttribute( + 'request.id', + Array.isArray(requestId) ? (requestId[0] ?? '') : requestId, + ); + } + + const tagPage = enhanceTagPage(req.body); + + span.setAttribute('tagPage.pageId', tagPage.pageId); + + const { html, prefetchScripts } = renderTagPage({ + tagPage, + }); + + span.setAttribute('response.htmlSizeBytes', html.length); + + traceSync('sendResponse', () => { + res.status(200) + .set('Link', makePrefetchHeader(prefetchScripts)) + .send(html); + }); }); - res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; diff --git a/dotcom-rendering/src/server/lib/tracing.ts b/dotcom-rendering/src/server/lib/tracing.ts new file mode 100644 index 00000000000..2abd021166a --- /dev/null +++ b/dotcom-rendering/src/server/lib/tracing.ts @@ -0,0 +1,119 @@ +import { context, type Span, SpanStatusCode, trace } from '@opentelemetry/api'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { RequestHandler } from 'express'; + +const OTEL_EXPORTER_ENDPOINT = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318'; + +export const initTracing = (): void => { + const exporter = new OTLPTraceExporter({ + url: `${OTEL_EXPORTER_ENDPOINT}/v1/traces`, + }); + + const provider = new BasicTracerProvider({ + resource: resourceFromAttributes({ + 'service.name': 'tag-page-rendering', + 'service.version': '1.0.0', + }), + spanProcessors: [new BatchSpanProcessor(exporter)], + }); + + // Register the context manager so that nested startActiveSpan calls + // produce proper parent-child relationships. + const contextManager = new AsyncLocalStorageContextManager(); + context.setGlobalContextManager(contextManager); + + trace.setGlobalTracerProvider(provider); + + console.log('OpenTelemetry tracing initialised'); + + process.on('SIGTERM', () => { + void provider.shutdown().then(() => { + console.log('OpenTelemetry tracing shut down'); + }); + }); +}; + +const tracer = trace.getTracer('tag-page-rendering'); + +/** + * Starts a span as a child of the current active context. + * The caller is responsible for ending the span — use this for async operations + * where the span must remain open across an async boundary. + */ +export const startSpan = (spanName: string): Span => { + return tracer.startSpan(spanName, undefined, context.active()); +}; + +/** + * Starts an active span for async operations. The span becomes the active + * context for any child spans created within the callback. The caller is + * responsible for calling span.end() when the async work completes. + */ +export const traceAsync = ( + spanName: string, + fn: (span: Span) => void, +): void => { + tracer.startActiveSpan(spanName, (span) => { + fn(span); + }); +}; + +/** + * Wraps a synchronous function in an OpenTelemetry span, recording its duration + * and any errors that occur. + */ +export const traceSync = (spanName: string, fn: (span: Span) => T): T => { + return tracer.startActiveSpan(spanName, (span) => { + try { + const result = fn(span); + span.setStatus({ code: SpanStatusCode.OK }); + return result; + } catch (error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: + error instanceof Error ? error.message : 'Unknown error', + }); + span.recordException( + error instanceof Error ? error : new Error(String(error)), + ); + throw error; + } finally { + span.end(); + } + }); +}; + +/** + * Express middleware that wraps the entire request lifecycle in a root span. + * Placed before body parsing so it captures JSON deserialization time and any + * event-loop queuing delays. + */ +export const tracingMiddleware: RequestHandler = (req, res, next) => { + const spanName = `HTTP ${req.method} ${req.path}`; + tracer.startActiveSpan(spanName, (span) => { + span.setAttribute('http.method', req.method); + span.setAttribute('http.path', req.path); + + const requestId = req.headers['x-request-id']; + if (requestId) { + span.setAttribute( + 'request.id', + Array.isArray(requestId) ? (requestId[0] ?? '') : requestId, + ); + } + + res.on('finish', () => { + span.setAttribute('http.status_code', res.statusCode); + span.setStatus({ code: SpanStatusCode.OK }); + span.end(); + }); + + next(); + }); +}; diff --git a/dotcom-rendering/src/server/render.front.web.tsx b/dotcom-rendering/src/server/render.front.web.tsx index d4256aa834f..db8f4f0c68e 100644 --- a/dotcom-rendering/src/server/render.front.web.tsx +++ b/dotcom-rendering/src/server/render.front.web.tsx @@ -19,6 +19,7 @@ import type { Config } from '../types/configContext'; import type { Front } from '../types/front'; import type { TagPage as TagPageModel } from '../types/tagPage'; import { htmlPageTemplate } from './htmlPageTemplate'; +import { traceSync } from './lib/tracing'; interface Props { front: Front; @@ -176,84 +177,102 @@ export const renderTagPage = ({ }: { tagPage: TagPageModel; }): { html: string; prefetchScripts: string[] } => { - const title = tagPage.webTitle; - const NAV = extractNAV(tagPage.nav); - const enhancedNAV = enhanceNav(NAV); + return traceSync('renderTagPage', (parentSpan) => { + const title = tagPage.webTitle; - const darkModeAvailable = - tagPage.config.serverSideABTests['webx-dark-mode-web'] === 'enable'; + const enhancedNAV = traceSync('extractAndEnhanceNav', () => { + const NAV = extractNAV(tagPage.nav); + return enhanceNav(NAV); + }); - // Fronts are not supported in Apps - const config: Config = { - renderingTarget: 'Web', - darkModeAvailable, - assetOrigin: ASSET_ORIGIN, - editionId: tagPage.editionId, - }; + const darkModeAvailable = + tagPage.config.serverSideABTests['webx-dark-mode-web'] === 'enable'; - const { html, extractedCss } = renderToStringWithEmotion( - - - , - ); + // Fronts are not supported in Apps + const config: Config = { + renderingTarget: 'Web', + darkModeAvailable, + assetOrigin: ASSET_ORIGIN, + editionId: tagPage.editionId, + }; - const build = getModulesBuild(); + const { html, extractedCss } = traceSync( + 'reactSSR.renderToStringWithEmotion', + (span) => { + const result = renderToStringWithEmotion( + + + , + ); + span.setAttribute('html.sizeBytes', result.html.length); + span.setAttribute('css.sizeBytes', result.extractedCss.length); + return result; + }, + ); - /** - * The highest priority scripts. - * These scripts have a considerable impact on site performance. - * Only scripts critical to application execution may go in here. - * Please talk to the dotcom platform team before adding more. - * Scripts will be executed in the order they appear in this array - */ - const prefetchScripts = [ - polyfillIO, - getPathFromManifest(build, 'frameworks.js'), - getPathFromManifest(build, 'index.js'), - process.env.COMMERCIAL_BUNDLE_URL ?? tagPage.config.commercialBundleUrl, - ].filter(isString); + const { prefetchScripts, scriptTags } = traceSync( + 'resolveAssets', + () => { + const b = getModulesBuild(); + const ps = [ + polyfillIO, + getPathFromManifest(b, 'frameworks.js'), + getPathFromManifest(b, 'index.js'), + process.env.COMMERCIAL_BUNDLE_URL ?? + tagPage.config.commercialBundleUrl, + ].filter(isString); + const st = generateScriptTags(ps); + return { prefetchScripts: ps, scriptTags: st }; + }, + ); - const scriptTags = generateScriptTags(prefetchScripts); + const guardian = traceSync('createGuardian', () => + createGuardian({ + editionId: tagPage.editionId, + stage: tagPage.config.stage, + frontendAssetsFullURL: tagPage.config.frontendAssetsFullURL, + revisionNumber: tagPage.config.revisionNumber, + sentryPublicApiKey: tagPage.config.sentryPublicApiKey, + sentryHost: tagPage.config.sentryHost, + keywordIds: tagPage.config.keywordIds, + dfpAccountId: tagPage.config.dfpAccountId, + adUnit: tagPage.config.adUnit, + ajaxUrl: tagPage.config.ajaxUrl, + googletagUrl: tagPage.config.googletagUrl, + switches: tagPage.config.switches, + serverSideABTests: tagPage.config.serverSideABTests, + brazeApiKey: tagPage.config.brazeApiKey, + googleRecaptchaSiteKey: tagPage.config.googleRecaptchaSiteKey, + unknownConfig: tagPage.config, + }), + ); - const guardian = createGuardian({ - editionId: tagPage.editionId, - stage: tagPage.config.stage, - frontendAssetsFullURL: tagPage.config.frontendAssetsFullURL, - revisionNumber: tagPage.config.revisionNumber, - sentryPublicApiKey: tagPage.config.sentryPublicApiKey, - sentryHost: tagPage.config.sentryHost, - keywordIds: tagPage.config.keywordIds, - dfpAccountId: tagPage.config.dfpAccountId, - adUnit: tagPage.config.adUnit, - ajaxUrl: tagPage.config.ajaxUrl, - googletagUrl: tagPage.config.googletagUrl, - switches: tagPage.config.switches, - serverSideABTests: tagPage.config.serverSideABTests, - brazeApiKey: tagPage.config.brazeApiKey, - googleRecaptchaSiteKey: tagPage.config.googleRecaptchaSiteKey, - // Until we understand exactly what config we need to make available client-side, - // add everything we haven't explicitly typed as unknown config - unknownConfig: tagPage.config, - }); + const section = tagPage.config.section; - const section = tagPage.config.section; + const pageHtml = traceSync('htmlPageTemplate', (span) => { + const result = htmlPageTemplate({ + scriptTags, + css: extractedCss, + html, + title, + description: tagPage.header.description, + guardian, + section, + renderingTarget: 'Web', + weAreHiring: !!tagPage.config.switches.weAreHiring, + canonicalUrl: tagPage.canonicalUrl, + config, + rssFeedUrl: tagPage.webURL + '/rss', + }); + span.setAttribute('pageHtml.sizeBytes', result.length); + return result; + }); - const pageHtml = htmlPageTemplate({ - scriptTags, - css: extractedCss, - html, - title, - description: tagPage.header.description, - guardian, - section, - renderingTarget: 'Web', - weAreHiring: !!tagPage.config.switches.weAreHiring, - canonicalUrl: tagPage.canonicalUrl, - config, - rssFeedUrl: tagPage.webURL + '/rss', + parentSpan.setAttribute('totalHtml.sizeBytes', pageHtml.length); + + return { + html: pageHtml, + prefetchScripts, + }; }); - return { - html: pageHtml, - prefetchScripts, - }; }; diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts index 62b75a3b639..7f43dbbbc54 100644 --- a/dotcom-rendering/src/server/server.prod.ts +++ b/dotcom-rendering/src/server/server.prod.ts @@ -32,13 +32,28 @@ import { responseHeaderMiddleware } from './lib/header-middleware'; import { logger } from './lib/logging'; import { requestLoggerMiddleware } from './lib/logging-middleware'; import { recordError } from './lib/logging-store'; +import { traceAsync, tracingMiddleware } from './lib/tracing'; export const prodServer = (): void => { logger.info('dotcom-rendering is GO.'); const app = express(); - app.use(express.json({ limit: '50mb' })); + app.use(tracingMiddleware); + app.use((req, res, next) => { + traceAsync('express.json', (span) => { + express.json({ limit: '50mb' })(req, res, (err) => { + if (req.body) { + span.setAttribute( + 'body.sizeBytes', + JSON.stringify(req.body).length, + ); + } + span.end(); + next(err); + }); + }); + }); app.use(requestLoggerMiddleware); app.use(compression()); app.use(responseHeaderMiddleware); diff --git a/dotcom-rendering/src/server/server.ts b/dotcom-rendering/src/server/server.ts index 9360ff1db39..f15036ccc98 100644 --- a/dotcom-rendering/src/server/server.ts +++ b/dotcom-rendering/src/server/server.ts @@ -1,4 +1,5 @@ import { devServer } from './server.dev'; +import { initTracing } from './lib/tracing'; import { prodServer } from './server.prod'; // this export is expected by webpack-hot-server-middleware @@ -8,5 +9,6 @@ export default devServer; // this is the actual production server if (process.env.NODE_ENV === 'production') { + initTracing(); prodServer(); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66ff3d3be70..a36279e7125 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,13 +219,13 @@ importers: version: link:../config '@sveltejs/adapter-auto': specifier: 7.0.1 - version: 7.0.1(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))) + version: 7.0.1(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))) '@sveltejs/adapter-static': specifier: 3.0.10 - version: 3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))) + version: 3.0.10(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))) '@sveltejs/kit': specifier: 2.60.1 - version: 2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) + version: 2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) '@sveltejs/vite-plugin-svelte': specifier: 7.1.2 version: 7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) @@ -368,6 +368,21 @@ importers: '@guardian/tsconfig': specifier: 'catalog:' version: 1.0.1 + '@opentelemetry/api': + specifier: ^1.9.1 + version: 1.9.1 + '@opentelemetry/context-async-hooks': + specifier: ^2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.220.0 + version: 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': + specifier: ^2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': + specifier: ^2.9.0 + version: 2.9.0(@opentelemetry/api@1.9.1) '@playwright/test': specifier: 1.60.0 version: 1.60.0 @@ -3070,6 +3085,78 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@opentelemetry/api-logs@0.220.0': + resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.9.0': + resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.9.0': + resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.220.0': + resolution: {integrity: sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.220.0': + resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.220.0': + resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.9.0': + resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.220.0': + resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.9.0': + resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.9.0': + resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace@2.9.0': + resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.43.0': + resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} + engines: {node: '>=14'} + '@package-json/types@0.0.12': resolution: {integrity: sha512-uu43FGU34B5VM9mCNjXCwLaGHYjXdNincqKLaraaCW+7S2+SmiBg1Nv8bPnmschrIfZmfKNY9f3fC376MRrObw==} @@ -12488,6 +12575,83 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + '@opentelemetry/api-logs@0.220.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + + '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.43.0 + + '@opentelemetry/semantic-conventions@1.43.0': {} + '@package-json/types@0.0.12': {} '@peculiar/asn1-cms@2.8.0': @@ -13413,15 +13577,15 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))': + '@sveltejs/adapter-auto@7.0.1(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))': dependencies: - '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) + '@sveltejs/kit': 2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))': dependencies: - '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) + '@sveltejs/kit': 2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)) - '@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))': + '@sveltejs/kit@2.60.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.3(@typescript-eslint/types@8.59.2))(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0)))(svelte@5.56.3(@typescript-eslint/types@8.59.2))(typescript@6.0.3)(vite@6.4.3(@types/node@24.12.4)(terser@5.49.0))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) @@ -13439,6 +13603,7 @@ snapshots: svelte: 5.56.3(@typescript-eslint/types@8.59.2) vite: 6.4.3(@types/node@24.12.4)(terser@5.49.0)(tsx@4.6.2) optionalDependencies: + '@opentelemetry/api': 1.9.1 typescript: 6.0.3 '@sveltejs/load-config@0.1.1': {} From be63224bb549eb5fbe9aa023fb6ed58e2f75e320 Mon Sep 17 00:00:00 2001 From: Jorge Azevedo Date: Fri, 17 Jul 2026 14:45:42 +0100 Subject: [PATCH 2/5] fix: pin OpenTelemetry dependency versions --- dotcom-rendering/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index 021f4a1b934..97af5ca9b7c 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -46,11 +46,11 @@ "@guardian/source-development-kitchen": "28.1.0", "@guardian/support-dotcom-components": "10.1.0", "@guardian/tsconfig": "catalog:", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/context-async-hooks": "^2.9.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", - "@opentelemetry/resources": "^2.9.0", - "@opentelemetry/sdk-trace-base": "^2.9.0", + "@opentelemetry/api": "1.9.1", + "@opentelemetry/context-async-hooks": "2.9.0", + "@opentelemetry/exporter-trace-otlp-http": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0", "@playwright/test": "1.60.0", "@sentry/browser": "10.52.0", "@storybook/addon-a11y": "10.3.3", From 01d12c8a33c4a7eda31530b302f39fd8c327585b Mon Sep 17 00:00:00 2001 From: Jorge Azevedo Date: Fri, 17 Jul 2026 14:50:58 +0100 Subject: [PATCH 3/5] fix: update lockfile for pinned OpenTelemetry versions --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a36279e7125..0d65e638630 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -369,19 +369,19 @@ importers: specifier: 'catalog:' version: 1.0.1 '@opentelemetry/api': - specifier: ^1.9.1 + specifier: 1.9.1 version: 1.9.1 '@opentelemetry/context-async-hooks': - specifier: ^2.9.0 + specifier: 2.9.0 version: 2.9.0(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-trace-otlp-http': - specifier: ^0.220.0 + specifier: 0.220.0 version: 0.220.0(@opentelemetry/api@1.9.1) '@opentelemetry/resources': - specifier: ^2.9.0 + specifier: 2.9.0 version: 2.9.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': - specifier: ^2.9.0 + specifier: 2.9.0 version: 2.9.0(@opentelemetry/api@1.9.1) '@playwright/test': specifier: 1.60.0 From 6b428ba86706767c666f61ed79542cacb4317636 Mon Sep 17 00:00:00 2001 From: Jorge Azevedo Date: Fri, 17 Jul 2026 14:58:55 +0100 Subject: [PATCH 4/5] fix: correct import order in server.ts --- dotcom-rendering/src/server/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotcom-rendering/src/server/server.ts b/dotcom-rendering/src/server/server.ts index f15036ccc98..910bd603d74 100644 --- a/dotcom-rendering/src/server/server.ts +++ b/dotcom-rendering/src/server/server.ts @@ -1,5 +1,5 @@ -import { devServer } from './server.dev'; import { initTracing } from './lib/tracing'; +import { devServer } from './server.dev'; import { prodServer } from './server.prod'; // this export is expected by webpack-hot-server-middleware From 195c0332da0cd71bcdf8ef95709ae61eaabbbd18 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Fri, 17 Jul 2026 17:11:13 +0100 Subject: [PATCH 5/5] feat: Enable "ssh" to the container --- .../cdk/lib/__snapshots__/renderingStack.test.ts.snap | 1 + dotcom-rendering/cdk/lib/renderingStack.ts | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index 291a8c367dc..89d0ff6ad65 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -2041,6 +2041,7 @@ exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE "Type": "ECS", }, "EnableECSManagedTags": false, + "EnableExecuteCommand": "true", "HealthCheckGracePeriodSeconds": 60, "LaunchType": "FARGATE", "LoadBalancers": [ diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index 5a33e13e5bb..44537a1b562 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -17,8 +17,9 @@ import { Metric, Unit } from 'aws-cdk-lib/aws-cloudwatch'; import { SnsAction } from 'aws-cdk-lib/aws-cloudwatch-actions'; import type { InstanceType } from 'aws-cdk-lib/aws-ec2'; import { Peer } from 'aws-cdk-lib/aws-ec2'; -import { ClusterSettings } from 'aws-cdk-lib/aws-ecs/mixins'; import { ContainerImage, LogDrivers } from 'aws-cdk-lib/aws-ecs'; +import type { CfnService } from 'aws-cdk-lib/aws-ecs'; +import { ClusterSettings } from 'aws-cdk-lib/aws-ecs/mixins'; import { Subscription, SubscriptionProtocol, Topic } from 'aws-cdk-lib/aws-sns'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { getUserData } from './userData'; @@ -323,6 +324,10 @@ export class RenderingCDKStack extends CDKStack { ); } + // Enable us to "ssh" to the container + const ecsService = app.ecsService?.node.defaultChild as CfnService; + ecsService.addPropertyOverride('EnableExecuteCommand', 'true'); + // TODO enable this at the pattern level in GuCDK app.ecsService?.cluster.with( new ClusterSettings([