@@ -85,6 +85,7 @@ interface ProjectionContext {
8585 /** Content fields omitted so far; a span compares it across its own fields to set `contentWithheld`. */
8686 omittedContentFields : number
8787 logDetails : ProjectionLogDetails
88+ displayFieldsBySpanId ?: ReadonlyMap < string , string >
8889}
8990
9091interface OversizedHydrationGate {
@@ -124,6 +125,11 @@ export interface ProjectTraceSpansForSecretsOptions {
124125 store : LargeValueStoreContext
125126 /** Read-only display projections omit ref-backed content instead of writing another ref. */
126127 allowLargeValueWrites ?: boolean
128+ /**
129+ * Synthetic spans that each carry one display-envelope value, keyed by span id. Warnings name
130+ * the envelope field instead of the synthetic span and count it apart from real spans.
131+ */
132+ displayFieldsBySpanId ?: ReadonlyMap < string , string >
127133}
128134
129135class TraceSecretProjectionError extends Error {
@@ -188,7 +194,9 @@ function describeProjectionFailure(error: unknown): { name: string; reason?: str
188194function createProjectionContext (
189195 matcher : ResolvedSecretMatcher ,
190196 store : LargeValueStoreContext ,
191- allowLargeValueWrites : boolean
197+ allowLargeValueWrites : boolean ,
198+ logDetails : ProjectionLogDetails ,
199+ displayFieldsBySpanId ?: ReadonlyMap < string , string >
192200) : ProjectionContext {
193201 return {
194202 matcher,
@@ -206,7 +214,8 @@ function createProjectionContext(
206214 sourceLargeValueBytes : 0 ,
207215 storedLargeValueBytes : 0 ,
208216 storedLargeValueCount : 0 ,
209- logDetails : projectionLogDetails ( store , allowLargeValueWrites ? 'write' : 'read' ) ,
217+ logDetails,
218+ displayFieldsBySpanId,
210219 }
211220}
212221
@@ -849,11 +858,12 @@ async function sanitizeContentField(
849858 try {
850859 return await sanitizeMaterializedValue ( value , context )
851860 } catch ( error ) {
861+ const displayField = context . displayFieldsBySpanId ?. get ( owner . id )
852862 logger . warn ( 'Omitting trace content that could not be sanitized' , {
853863 ...context . logDetails ,
854- spanId : owner . id ,
855- spanType : owner . type ,
856- field ,
864+ ... ( displayField === undefined
865+ ? { spanId : owner . id , spanType : owner . type , field }
866+ : { displayField } ) ,
857867 failure : describeProjectionFailure ( error ) ,
858868 } )
859869 context . omittedContentFields += 1
@@ -1269,32 +1279,41 @@ function projectBoundedTraceSpans(
12691279 state : BoundedTraceStructureState ,
12701280 depth : number
12711281 ) => TraceSpan | undefined ,
1282+ logDetails : ProjectionLogDetails ,
12721283 state : BoundedTraceStructureState = { nodes : 0 , truncated : false }
12731284) : TraceSpan [ ] {
12741285 const projected = projectBoundedArray ( traceSpans , ( span ) => project ( span , state , 0 ) )
12751286 if ( state . truncated ) {
1276- logger . warn ( 'Trace structure exceeded projection limits; truncating projected structure' )
1287+ logger . warn (
1288+ 'Trace structure exceeded projection limits; truncating projected structure' ,
1289+ logDetails
1290+ )
12771291 }
12781292 return projected
12791293}
12801294
12811295/** Pass `state` to continue a structure budget that earlier spans of the same trace already spent. */
12821296function structuralOnlyTraceSpans (
12831297 traceSpans : TraceSpan [ ] ,
1298+ logDetails : ProjectionLogDetails ,
12841299 state ?: BoundedTraceStructureState
12851300) : TraceSpan [ ] {
12861301 try {
1287- return projectBoundedTraceSpans ( traceSpans , structuralOnlySpan , state )
1302+ return projectBoundedTraceSpans ( traceSpans , structuralOnlySpan , logDetails , state )
12881303 } catch ( error ) {
12891304 logger . warn ( 'Trace structure could not be safely traversed; omitting projected spans' , {
1305+ ...logDetails ,
12901306 failure : describeProjectionFailure ( error ) ,
12911307 } )
12921308 return [ ]
12931309 }
12941310}
12951311
1296- function cloneTraceSpansForProjection ( traceSpans : TraceSpan [ ] ) : TraceSpan [ ] {
1297- return projectBoundedTraceSpans ( traceSpans , cloneTraceSpanForProjection )
1312+ function cloneTraceSpansForProjection (
1313+ traceSpans : TraceSpan [ ] ,
1314+ logDetails : ProjectionLogDetails
1315+ ) : TraceSpan [ ] {
1316+ return projectBoundedTraceSpans ( traceSpans , cloneTraceSpanForProjection , logDetails )
12981317}
12991318
13001319function assertNoPlaintext (
@@ -1600,9 +1619,10 @@ async function verifyPostTransformLargeValues(
16001619async function assertPostTransformTraceSpansAreSafe (
16011620 traceSpans : TraceSpan [ ] ,
16021621 matcher : ResolvedSecretMatcher ,
1603- store : LargeValueStoreContext
1622+ store : LargeValueStoreContext ,
1623+ logDetails : ProjectionLogDetails
16041624) : Promise < void > {
1605- const projection = createProjectionContext ( matcher , store , false )
1625+ const projection = createProjectionContext ( matcher , store , false , logDetails )
16061626 const context : PostTransformInvariantContext = {
16071627 projection,
16081628 plaintext : {
@@ -1623,6 +1643,32 @@ async function assertPostTransformTraceSpansAreSafe(
16231643 await verifyPostTransformLargeValues ( refs , context , new Set < string > ( ) )
16241644}
16251645
1646+ /**
1647+ * Counts the real spans kept before `withheldFrom` and withheld from it on, naming the withheld
1648+ * display-envelope fields apart from them.
1649+ */
1650+ function describeWithheldContent (
1651+ traceSpans : readonly TraceSpan [ ] ,
1652+ withheldFrom : number ,
1653+ displayFieldsBySpanId : ReadonlyMap < string , string > | undefined
1654+ ) : { retainedSpanCount : number ; withheldSpanCount : number ; withheldDisplayFields ?: string [ ] } {
1655+ let retainedDisplayFieldCount = 0
1656+ const withheldDisplayFields : string [ ] = [ ]
1657+ if ( displayFieldsBySpanId ?. size ) {
1658+ for ( let index = 0 ; index < traceSpans . length ; index += 1 ) {
1659+ const displayField = displayFieldsBySpanId . get ( traceSpans [ index ] . id )
1660+ if ( displayField === undefined ) continue
1661+ if ( index < withheldFrom ) retainedDisplayFieldCount += 1
1662+ else withheldDisplayFields . push ( displayField )
1663+ }
1664+ }
1665+ return {
1666+ retainedSpanCount : withheldFrom - retainedDisplayFieldCount ,
1667+ withheldSpanCount : traceSpans . length - withheldFrom - withheldDisplayFields . length ,
1668+ ...( withheldDisplayFields . length > 0 ? { withheldDisplayFields } : { } ) ,
1669+ }
1670+ }
1671+
16261672export interface EnforceTraceSpanSecretInvariantOptions {
16271673 registry ?: ResolvedSecretTraceRegistry
16281674 store : LargeValueStoreContext
@@ -1638,22 +1684,23 @@ export async function enforceTraceSpanSecretInvariant(
16381684 traceSpans : TraceSpan [ ] ,
16391685 options : EnforceTraceSpanSecretInvariantOptions
16401686) : Promise < TraceSpan [ ] > {
1687+ const logDetails = projectionLogDetails ( options . store , 'write' )
16411688 try {
1642- if ( ! options . registry ?. isComplete ( ) ) return structuralOnlyTraceSpans ( traceSpans )
1689+ if ( ! options . registry ?. isComplete ( ) ) return structuralOnlyTraceSpans ( traceSpans , logDetails )
16431690
16441691 const matcher = createResolvedSecretMatcher ( options . registry . getActiveMatches ( ) , {
16451692 preserveNamedProvenanceLabels : true ,
16461693 } )
16471694 if ( ! matcher ) return traceSpans
16481695
1649- await assertPostTransformTraceSpansAreSafe ( traceSpans , matcher , options . store )
1696+ await assertPostTransformTraceSpansAreSafe ( traceSpans , matcher , options . store , logDetails )
16501697 return traceSpans
16511698 } catch ( error ) {
16521699 logger . warn ( 'Trace secret invariant failed; retaining structural spans only' , {
1653- ...projectionLogDetails ( options . store , 'write' ) ,
1700+ ...logDetails ,
16541701 failure : describeProjectionFailure ( error ) ,
16551702 } )
1656- return structuralOnlyTraceSpans ( traceSpans )
1703+ return structuralOnlyTraceSpans ( traceSpans , logDetails )
16571704 }
16581705}
16591706
@@ -1665,19 +1712,25 @@ export async function projectTraceSpansForSecrets(
16651712 traceSpans : TraceSpan [ ] ,
16661713 options : ProjectTraceSpansForSecretsOptions
16671714) : Promise < TraceSpan [ ] > {
1715+ const allowLargeValueWrites = options . allowLargeValueWrites !== false
1716+ const logDetails = projectionLogDetails ( options . store , allowLargeValueWrites ? 'write' : 'read' )
16681717 if ( ! options . registry ?. isComplete ( ) ) {
1669- return structuralOnlyTraceSpans ( traceSpans )
1718+ return structuralOnlyTraceSpans ( traceSpans , logDetails )
16701719 }
16711720
1672- const allowLargeValueWrites = options . allowLargeValueWrites !== false
1673- const logDetails = projectionLogDetails ( options . store , allowLargeValueWrites ? 'write' : 'read' )
16741721 try {
16751722 const matcher = createResolvedSecretMatcher ( options . registry . getActiveMatches ( ) , {
16761723 preserveNamedProvenanceLabels : true ,
16771724 } )
1678- if ( ! matcher ) return cloneTraceSpansForProjection ( traceSpans )
1725+ if ( ! matcher ) return cloneTraceSpansForProjection ( traceSpans , logDetails )
16791726
1680- const context = createProjectionContext ( matcher , options . store , allowLargeValueWrites )
1727+ const context = createProjectionContext (
1728+ matcher ,
1729+ options . store ,
1730+ allowLargeValueWrites ,
1731+ logDetails ,
1732+ options . displayFieldsBySpanId
1733+ )
16811734
16821735 const projected : TraceSpan [ ] = [ ]
16831736 const state : BoundedTraceStructureState = { nodes : 0 , truncated : false }
@@ -1698,17 +1751,16 @@ export async function projectTraceSpansForSecrets(
16981751 * spans left it, so the trace keeps exactly the structure a whole-trace fallback would.
16991752 */
17001753 logger . warn (
1701- 'Trace content exceeded the projection budget; withholding the remaining spans ' ,
1754+ 'Trace content exceeded the projection budget; withholding the remaining content ' ,
17021755 {
17031756 ...logDetails ,
1704- retainedSpanCount : index ,
1705- withheldSpanCount : traceSpans . length - index ,
1757+ ...describeWithheldContent ( traceSpans , index , options . displayFieldsBySpanId ) ,
17061758 failure : describeProjectionFailure ( error ) ,
17071759 }
17081760 )
17091761 return [
17101762 ...projected ,
1711- ...structuralOnlyTraceSpans ( traceSpans . slice ( index ) , {
1763+ ...structuralOnlyTraceSpans ( traceSpans . slice ( index ) , logDetails , {
17121764 nodes : retainedStructureNodes ,
17131765 truncated : false ,
17141766 } ) ,
@@ -1721,6 +1773,6 @@ export async function projectTraceSpansForSecrets(
17211773 ...logDetails ,
17221774 failure : describeProjectionFailure ( error ) ,
17231775 } )
1724- return structuralOnlyTraceSpans ( traceSpans )
1776+ return structuralOnlyTraceSpans ( traceSpans , logDetails )
17251777 }
17261778}
0 commit comments