11import Fastify , { FastifyBaseLogger } from 'fastify' ;
22import { S3Client } from '@aws-sdk/client-s3' ;
33import { fastifyConnectPlugin } from '@connectrpc/connect-fastify' ;
4+ import * as Sentry from '@sentry/node' ;
45import { cors , createContextValues } from '@connectrpc/connect' ;
56import fastifyCors from '@fastify/cors' ;
67import { pino , stdTimeFunctions , LoggerOptions } from 'pino' ;
@@ -37,7 +38,13 @@ import { BillingRepository } from './repositories/BillingRepository.js';
3738import { BillingService } from './services/BillingService.js' ;
3839import { UserRepository } from './repositories/UserRepository.js' ;
3940import { AIGraphReadmeQueue , createAIGraphReadmeWorker } from './workers/AIGraphReadmeWorker.js' ;
40- import { fastifyLoggerId , createS3ClientConfig , extractS3BucketName , isGoogleCloudStorageUrl } from './util.js' ;
41+ import {
42+ fastifyLoggerId ,
43+ sentrySpanId ,
44+ createS3ClientConfig ,
45+ extractS3BucketName ,
46+ isGoogleCloudStorageUrl ,
47+ } from './util.js' ;
4148import { ApiKeyRepository } from './repositories/ApiKeyRepository.js' ;
4249import { createDeleteOrganizationWorker , DeleteOrganizationQueue } from './workers/DeleteOrganizationWorker.js' ;
4350import {
@@ -531,6 +538,16 @@ export default async function build(opts: BuildConfig) {
531538 keycloakRealm : opts . keycloak . realm ,
532539 } ) ;
533540
541+ // Capture the active Sentry span in preHandler (where OTEL context is still available)
542+ // and store it on the request so Connect interceptors can use it as parentSpan.
543+ fastify . addHook ( 'preHandler' , ( req , _reply , done ) => {
544+ const span = Sentry . getActiveSpan ( ) ;
545+ if ( span ) {
546+ ( req . raw as any ) . __sentrySpan = span ;
547+ }
548+ done ( ) ;
549+ } ) ;
550+
534551 // Must be registered after custom fastify routes
535552 // Because it registers an all-catch route for connect handlers
536553
@@ -567,7 +584,16 @@ export default async function build(opts: BuildConfig) {
567584 cdnBaseUrl : opts . cdnBaseUrl ,
568585 } ) ,
569586 contextValues ( req ) {
570- return createContextValues ( ) . set < FastifyBaseLogger > ( { id : fastifyLoggerId , defaultValue : req . log } , req . log ) ;
587+ const values = createContextValues ( ) . set < FastifyBaseLogger > (
588+ { id : fastifyLoggerId , defaultValue : req . log } ,
589+ req . log ,
590+ ) ;
591+ // Read the parent span captured during the preHandler hook
592+ const parentSpan = ( req . raw as any ) . __sentrySpan ;
593+ if ( parentSpan ) {
594+ values . set ( { id : sentrySpanId , defaultValue : undefined } , parentSpan ) ;
595+ }
596+ return values ;
571597 } ,
572598 logLevel : opts . logger . level as pino . LevelWithSilent ,
573599 // Avoid compression for small requests
@@ -578,6 +604,18 @@ export default async function build(opts: BuildConfig) {
578604 // We go with 32MiB to avoid allocating too much memory for large requests
579605 writeMaxBytes : 32 * 1024 * 1024 ,
580606 acceptCompression : [ compressionBrotli , compressionGzip ] ,
607+ interceptors : [
608+ ( next ) => ( req ) => {
609+ const parentSpan = req . contextValues ?. get ( {
610+ id : sentrySpanId ,
611+ defaultValue : undefined ,
612+ } ) ;
613+ if ( parentSpan ) {
614+ return Sentry . withActiveSpan ( parentSpan , ( ) => next ( req ) ) ;
615+ }
616+ return next ( req ) ;
617+ } ,
618+ ] ,
581619 } ) ;
582620
583621 await fastify . register ( fastifyGracefulShutdown , {
0 commit comments