2026-10-13 10:33:31 +02:00
'use strict' ;
2026-10-13 16:58:15 +02:00
const { validateStages } = require ( '../../packages/core/inspection-stages.cjs' );
2026-10-13 10:33:31 +02:00
const { AsyncLocalStorage } = require ( 'node:async_hooks' );
const { performance } = require ( 'node:perf_hooks' );
const { randomInt } = require ( 'node:crypto' );
const http = require ( 'node:http' ), https = require ( 'node:https' );
2026-10-13 23:19:22 +02:00
let telemetryModules ;
function loadTelemetryModules (){ return telemetryModules ??= {... require ( '@opentelemetry/sdk-trace' ),... require ( '@opentelemetry/sdk-logs' ),... require ( '@opentelemetry/resources' ),... require ( '@opentelemetry/exporter-trace-otlp-http' ),... require ( '@opentelemetry/exporter-logs-otlp-http' )};}
2026-10-13 18:24:44 +02:00
const { trace , ROOT_CONTEXT , SpanStatusCode , TraceFlags } = require ( '@opentelemetry/api' );
2026-10-13 18:48:40 +02:00
const names = new Set ([ 'browser.window' , 'browser.network' , 'browser.ipc' , 'browser.agent' , 'browser.management' , 'browser.navigation' , 'browser.dns' , 'browser.request' , 'browser.request.headers' , 'browser.inspect.local' , 'browser.inspect.enterprise' , 'browser.download' , 'browser.clipboard' , 'browser.visual' , 'browser.password.fill' , 'browser.ai' , 'browser.policy' , 'browser.startup' , 'browser.parser' , 'browser.semantic' , 'browser.yara' , 'browser.icap' , 'browser.tab' , 'browser.security' , 'browser.evidence' ]);
2026-10-13 18:24:44 +02:00
const operations = new Set ([ 'audit.deliver' , 'evidence.observer.install' , 'evidence.observer.drain' , 'evidence.observer.dispose' , 'policy.verify' , 'policy.bookmarks' , 'policy.agent-reconcile' , 'policy.page-configure' , 'policy.audit' , 'evidence.capture' , 'evidence.authorize' , 'evidence.challenge' , 'evidence.encrypt' , 'evidence.send' , 'evidence.ack' , 'agent.request' , 'agent.challenge' , 'agent.decrypt' , 'agent.transport' , 'agent.authorize' , 'agent.audit' , 'agent.dispatch' , 'agent.response' , 'agent.project' , 'agent.encrypt' , 'library:organize-plan' , 'library:organize-apply' , 'application:state' , 'settings:get' , 'settings:update' , 'bookmarks:list' , 'bookmarks:mutate' , 'tabs:create' , 'tabs:activate' , 'tabs:close' , 'tabs:group' , 'state' , 'authorize-setting' , 'settings-update' , 'window-control' , 'chrome-layout' , 'share-url' , 'bookmarks-mutate' , 'enroll' , 'sso' , 'navigate' , 'back' , 'forward' , 'reload' , 'ai' , 'inspect-file' , 'agent-connection' , 'ai-panel' , 'vault-panel' , 'vault-status' , 'vault-create' , 'vault-unlock' , 'vault-lock' , 'vault-list' , 'vault-save' , 'vault-delete' , 'vault-fill' , 'vault-generate' , 'tab-create' , 'tab-activate' , 'tab-close' , 'tab-group' , 'sessions' , 'read' , 'inspect' , 'scroll' , 'click' , 'type' , 'select' , 'check' , 'screenshot' , 'copy' , 'cut' , 'paste' , 'scan' , 'download' , 'sync' , 'create' , 'activate' , 'close' , 'group' , 'policy' , 'request' , 'dns' , 'parse' , 'embed' , 'yara' , 'icap' ]);
2026-10-13 21:39:47 +02:00
for ( const operation of [ 'visual.probe' , 'visual.capture' , 'visual.inspect.local' , 'visual.inspect.enterprise' , 'visual.audit' , 'visual.validate' , 'visual.project' , 'visual.release' ]) operations . add ( operation );
2026-10-13 10:33:31 +02:00
const components = new Set ([ 'browser' , 'ipc' , 'agent' , 'management' , 'navigation' , 'network' , 'dns' , 'dlp' , 'inspector' , 'parser' , 'semantic' , 'yara' , 'icap' , 'download' , 'clipboard' , 'visual' , 'vault' , 'ai' , 'policy' , 'tabs' ]);
2026-10-13 23:19:22 +02:00
for ( const operation of [ 'text.local' , 'text.enterprise' ]) operations . add ( operation );
2026-10-13 18:48:40 +02:00
for ( const operation of [ 'history:list' , 'history:go' , 'window:state' , 'window:control' ]) operations . add ( operation );
2026-10-13 21:00:41 +02:00
names . add ( 'browser.vault' ); names . add ( 'browser.credential' );
for ( const operation of [ 'vault:prepare-submit' , 'vault-prepare-submit' , 'credential.issue' , 'credential.claim' , 'credential.headers' , 'credential.authorize' , 'credential.redirect' , 'credential.complete' , 'credential.failure' , 'credential.authority' , 'credential.native' , 'credential.parse' , 'credential.match' , 'credential.residual' , 'credential.header-values' ]) operations . add ( operation );
2026-10-13 19:14:32 +02:00
for ( const operation of [ 'vault:status' , 'vault:list' , 'vault:fill' , 'vault.inventory.inspect' , 'vault.fill.visual' , 'vault.fill.native' ]) operations . add ( operation );
2026-10-13 19:41:27 +02:00
// Session collection stages use fixed identifiers; names and URLs are never attributes.
names . add ( 'browser.session' );
components . add ( 'sessions' );
for ( const operation of [ 'sessions:save' , 'sessions:list' , 'sessions:restore' , 'sessions.inspect' , 'sessions.load' , 'sessions.list' , 'sessions.save' , 'sessions.restore' , 'sessions.authorize' , 'sessions.preflight' , 'sessions.native' , 'sessions.rollback' ]) operations . add ( operation );
for ( const operation of [ 'audit.serialize' , 'audit.encrypt' , 'audit.write' , 'audit.fsync' , 'audit.commit' ]) operations . add ( operation );
2026-10-13 10:33:31 +02:00
const decisions = new Set ([ 'allow' , 'block' , 'deny' , 'unknown' ]), statuses = new Set ([ 'ok' , 'error' , 'cancelled' , 'timeout' ]);
const requestTypes = new Set ([ 'mainFrame' , 'subFrame' , 'stylesheet' , 'script' , 'image' , 'font' , 'object' , 'xhr' , 'ping' , 'cspReport' , 'media' , 'webSocket' , 'other' ]);
2026-10-13 16:58:15 +02:00
for ( const name of [ 'browser.upload.lease' , 'browser.upload.part' , 'browser.upload.release' , 'browser.upload.fault' ]) names . add ( name );
// Diagnostic reasons are fixed code identifiers, never exception messages,
// policy rule text, request content or arbitrary caller-supplied labels.
const reasons = new Set ([ 'request-source-context-changed' , 'header-source-context-changed' , 'headers-inspection-timeout' , 'request-inspection-timeout' , 'upload-lease-lost' , 'upload-lease-release-failed' , 'invalid-source-owner' , 'policy-changed' ]);
2026-10-14 00:37:32 +02:00
names . add ( 'browser.transport' );
for ( const operation of [ 'transport.install' , 'transport.target' , 'transport.dispose' , 'transport.created' , 'transport.closed' , 'transport.established' , 'transport.monitor-unavailable' ]) operations . add ( operation );
// Keep the diagnostics cold path free of schema/SDK initialization.
for ( const reason of [ 'transport-guard-unavailable' , 'transport-observer-detached' , 'transport-observer-command-timeout' , 'transport-observer-overflow' , 'transport-observer-audit-failed' , 'transport-observer-unavailable' , 'unexpected-webtransport-established' ]) reasons . add ( reason );
2026-10-13 10:33:31 +02:00
const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i ;
function attributes ( input ){ const out = {}; if ( ! input || typeof input !== 'object' ) return out ; for ( const [ key , descriptor ] of Object . entries ( Object . getOwnPropertyDescriptors ( input ))){ if ( ! ( 'value' in descriptor )) continue ; const value = descriptor . value ;
if ([ 'sessionId' , 'deviceId' ]. includes ( key ) && typeof value === 'string' && uuid . test ( value )) out [ key === 'sessionId' ? 'safetynet.session.id' : 'safetynet.device.id' ] = value ;
else if ([ 'policyVersion' , 'bytes' , 'count' ]. includes ( key ) && Number . isSafeInteger ( value ) && value >= 0 ) out [ key === 'policyVersion' ? 'safetynet.policy.version' : `safetynet. ${ key } ` ] = value ;
else if ( key === 'durationMs' && Number . isFinite ( value ) && value >= 0 && value <= 86400000 ) out [ 'safetynet.duration.ms' ] = value ;
else if ( key === 'operation' && operations . has ( value )) out [ 'safetynet.operation' ] = value ;
else if ( key === 'component' && components . has ( value )) out [ 'safetynet.component' ] = value ;
else if ( key === 'decision' && decisions . has ( value )) out [ 'dlp.decision' ] = value ;
else if ( key === 'status' && statuses . has ( value )) out [ 'safetynet.status' ] = value ;
else if ( key === 'requestType' && requestTypes . has ( value )) out [ 'safetynet.request.type' ] = value ;
2026-10-13 16:58:15 +02:00
else if ( key === 'reason' && reasons . has ( value )) out [ 'safetynet.reason' ] = value ;
2026-10-13 10:33:31 +02:00
} return out ;}
function endpointAllowed ( value ){ try { const u = new URL ( value ); if ( u . username || u . password || u . search || u . hash ) return false ; const host = u . hostname . replace ( /^\[|\]$/g , '' ); return u . protocol === 'https:' || ( u . protocol === 'http:' && ( host === '::1' || /^127\.(?:\d{1,3}\.){2}\d{1,3}$/ . test ( host ) && host . split ( '.' ). every ( p => + p <= 255 )));} catch { return false ;}}
function valid ( config ){ return config && config . enabled === true && [ 'operational' , 'debug' ]. includes ( config . mode ) && endpointAllowed ( config . endpoint ) && [ 'sampleRate' , 'debugSampleRate' ]. every ( k => Number . isFinite ( config [ k ]) && config [ k ] >= 0 && config [ k ] <= 1 ) && Number . isSafeInteger ( config . maxQueueSize ) && config . maxQueueSize >= 16 && config . maxQueueSize <= 4096 && Number . isSafeInteger ( config . flushIntervalMs ) && config . flushIntervalMs >= 250 && config . flushIntervalMs <= 10000 && Number . isSafeInteger ( config . maxRecordAgeMs ) && config . maxRecordAgeMs >= 1000 && config . maxRecordAgeMs <= 300000 && Number . isSafeInteger ( config . exportTimeoutMs ) && config . exportTimeoutMs >= 250 && config . exportTimeoutMs <= 10000 ;}
const defaults = { enabled : false , mode : 'operational' , sampleRate : 0.1 , debugSampleRate : 1 , maxQueueSize : 512 , flushIntervalMs : 2000 , maxRecordAgeMs : 30000 , exportTimeoutMs : 2000 };
2026-10-14 00:37:32 +02:00
function createDiagnostics (){ const storage = new AsyncLocalStorage (); let generation = null , configurationKey = '' , closed = false ; const retiring = new Set (); let activeSpans = 0 , lifetimeFailedExports = 0 , generationOrdinal = 0 ; const failureReasons = Object . create ( null ), failureFacts = [];
2026-10-13 16:58:15 +02:00
// Parser timing is untrusted metadata: validate the entire batch before creating
// spans, and attach it only to a currently recording browser operation.
function importStages ( envelope ){ try { const current = generation , parent = storage . getStore (); if ( ! current ? . active || current . config . mode !== 'debug' || parent ? . generation !== current ||! parent . span . isRecording () || activeSpans >= 128 ) return ;
const batch = validateStages ( envelope ); if ( ! batch ) return ; const base = batch . baseUnixMs , validated = batch . records . map ( item =>({... item , start : item . startOffsetMs , duration : item . durationMs }));
2026-10-14 01:18:23 +02:00
const imported = new Map (); for ( const item of validated ){ if ( ! current . active || generation !== current ||! parent . span . isRecording ()) return ; const ancestor = item . parentId === null ? parent . span : imported . get ( item . parentId ); if ( ! ancestor ) return ; const child = current . tracer . startSpan ( `inspector. ${ item . stage } ` ,{ startTime : base + item . start , attributes : {...( parent . context || current . context ),... attributes ({ component : 'inspector' , status : item . status , durationMs : item . duration })}}, trace . setSpan ( ROOT_CONTEXT , ancestor )); imported . set ( item . id , child ); child . setStatus ({ code : item . status === 'ok' ? SpanStatusCode . OK : SpanStatusCode . ERROR }); child . end ( base + item . start + item . duration );}
2026-10-13 16:58:15 +02:00
} catch { /* Diagnostics must never change inspection results. */ }}
2026-10-14 01:18:23 +02:00
function retire ( current ){ if ( ! current ) return ; current . active = false ; current . retiredAt = Date . now (); for ( const cancel of current . cancelExports || []) cancel (); for ( const [ request , attempt ] of current . requests ){ attempt . cancelSignalled = true ; const error = Error ( 'Diagnostics cancelled' ); error . code = 'SAFETYNET_DIAGNOSTICS_CANCELLED' ; request . destroy ( error );} for ( const agent of current . agents ) agent . destroy (); const pending = Promise . allSettled ([ current . tracerProvider ? . shutdown (), current . loggerProvider ? . shutdown ()]); retiring . add ( pending ); pending . finally (()=> retiring . delete ( pending ));}
function failureReason ( error , attempt ){ const code = error ? . code ; if ( code === 'SAFETYNET_DIAGNOSTICS_QUEUE_OVERFLOW' ) return 'queue-overflow' ; if ( code === 'SAFETYNET_DIAGNOSTICS_QUEUE_EXPIRED' ) return 'queue-expired' ; if ( code === 'SAFETYNET_DIAGNOSTICS_CANCELLED' || attempt . nativeCode === 'SAFETYNET_DIAGNOSTICS_CANCELLED' ) return 'cancelled' ; if ( code === 'ETIMEDOUT' || error ? . message === 'Request timed out' ) return 'timeout' ; if ( code === 'ECONNREFUSED' || attempt . nativeCode === 'ECONNREFUSED' ) return 'connection-refused' ; if ( code === 'ECONNRESET' || attempt . nativeCode === 'ECONNRESET' ) return 'connection-reset' ; if ( error ? . message === 'Concurrent export limit reached' ) return 'concurrency-limit' ; const status = Number . isInteger ( code ) ? code : attempt . httpStatus ; if ( status >= 300 && status < 400 ) return 'http-redirect' ; if ( status >= 400 && status < 500 ) return 'http-client' ; if ( status >= 500 && status < 600 ) return 'http-server' ; return 'exporter-failure' ;}
function gatedExporter ( exporter , current , signal ){
// Each signal owns one native export. Pending records are bounded independently
// of the SDK processor; expiry uses original timestamps, never enqueue refresh.
const queue = []; let running = null , queuedRecords = 0 , cooling = false ;
const fixedError = code => Object . assign ( Error ( 'Diagnostics export failed' ),{ code });
const complete = ( job , result )=>{ if ( job . finished ) return ; job . finished = true ; clearTimeout ( job . timer ); if ( current . attempts [ signal ] === job . attempt ) delete current . attempts [ signal ]; if ( result . code !== 0 ){ current . failedExports ++ ; lifetimeFailedExports ++ ; const reason = failureReason ( result . error , job . attempt ); failureReasons [ reason ] = ( failureReasons [ reason ] || 0 ) + 1 ; failureFacts . push ({ signal , generation : current . ordinal , reason , durationMs : Math . max ( 0 , Date . now () - job . attempt . startedAt ), queueAgeMs : Math . max ( 0 ,( job . dispatchedAt || Date . now ()) - job . attempt . startedAt ), activeAtCompletion : current . active , retirementOverlapped :!! current . retiredAt && current . retiredAt >= job . attempt . startedAt , cancelSignalled : job . attempt . cancelSignalled , httpStatus : job . attempt . httpStatus }); if ( failureFacts . length > 32 ) failureFacts . shift ();} try { job . callback ( result );} finally { if ( running === job ){ running = null ; cooling = true ; setImmediate (()=>{ cooling = false ; drain ();});} else setImmediate ( drain );}};
const drain = ()=>{ if ( running || cooling ||! current . active ) return ; const job = queue . shift (); if ( ! job ) return ; queuedRecords -= job . records . length ; clearTimeout ( job . timer ); if ( Date . now () >= job . expiresAt ){ complete ( job ,{ code : 1 , error : fixedError ( 'SAFETYNET_DIAGNOSTICS_QUEUE_EXPIRED' )}); return ;} running = job ; job . dispatchedAt = Date . now (); current . attempts [ signal ] = job . attempt ; try { exporter . export ( job . records , result => complete ( job , result ));} catch ( error ){ complete ( job ,{ code : 1 , error });}};
current . cancelExports . add (()=>{ for ( const job of queue . splice ( 0 )){ queuedRecords -= job . records . length ; job . attempt . cancelSignalled = true ; complete ( job ,{ code : 1 , error : fixedError ( 'SAFETYNET_DIAGNOSTICS_CANCELLED' )});} if ( running ){ const job = running ; job . attempt . cancelSignalled = true ; complete ( job ,{ code : 1 , error : fixedError ( 'SAFETYNET_DIAGNOSTICS_CANCELLED' )});}});
return { export ( records , callback ){ if ( ! current . active ){ callback ({ code : 0 }); return ;} const now = Date . now (), fresh = records . filter ( record =>{ const timestamp = record . endTime || record . hrTime || record . hrTimeObserved ; return timestamp && now - ( timestamp [ 0 ] * 1000 + timestamp [ 1 ] / 1e6 ) <= current . config . maxRecordAgeMs ;}); if ( ! fresh . length ){ callback ({ code : 0 }); return ;} const expiresAt = fresh . reduce (( limit , record )=>{ const stamp = record . endTime || record . hrTime || record . hrTimeObserved ; return Math . min ( limit , stamp [ 0 ] * 1000 + stamp [ 1 ] / 1e6 + current . config . maxRecordAgeMs );}, now + current . config . maxRecordAgeMs ); const job = { records : fresh , callback , expiresAt , attempt : { startedAt : now , httpStatus : null , nativeCode : null , cancelSignalled : false }, finished : false }; if ( queuedRecords + fresh . length > current . config . maxQueueSize ){ complete ( job ,{ code : 1 , error : fixedError ( 'SAFETYNET_DIAGNOSTICS_QUEUE_OVERFLOW' )}); return ;} queue . push ( job ); queuedRecords += fresh . length ; job . timer = setTimeout (()=>{ const index = queue . indexOf ( job ); if ( index < 0 ) return ; queue . splice ( index , 1 ); queuedRecords -= job . records . length ; complete ( job ,{ code : 1 , error : fixedError ( 'SAFETYNET_DIAGNOSTICS_QUEUE_EXPIRED' )});}, Math . max ( 1 , Math . ceil ( expiresAt - now ))); job . timer . unref ? .(); drain ();}, shutdown : ()=> exporter . shutdown (), forceFlush : ()=> exporter . forceFlush ? .() || Promise . resolve ()};
}
2026-10-14 00:37:32 +02:00
2026-10-14 01:18:23 +02:00
function canContinue ( input = {}, context = {}){ if ( closed ||! generation ? . active ) return false ; const config = {... defaults ,... input }; if ( ! valid ( config ) || JSON . stringify ( config ) !== JSON . stringify ( generation . config )) return false ; const identity = value => Object . fromEntries ( Object . entries ( attributes ( value )). filter (([ key ])=> key !== 'safetynet.policy.version' ). sort (([ a ],[ b ])=> a . localeCompare ( b ))); return JSON . stringify ( identity ( context )) === JSON . stringify ( Object . fromEntries ( Object . entries ( generation . context ). filter (([ key ])=> key !== 'safetynet.policy.version' ). sort (([ a ],[ b ])=> a . localeCompare ( b ))));}
function configure ( input = {}, context = {}){ if ( closed ) return false ; const config = {... defaults ,... input }; const key = JSON . stringify ([ config , attributes ( context )]); if ( key === configurationKey ) return !! generation ? . active ; configurationKey = key ; if ( canContinue ( input , context )){ generation . context = Object . freeze ( attributes ( context )); return true ;} const previous = generation ; generation = null ; retire ( previous ); if ( ! valid ( config ) || retiring . size >= 4 ) return false ;
let current ; try { current = { active : true , config , context : Object . freeze ( attributes ( context )), agents : new Set (), cancelExports : new Set (), requests : new Map (), attempts : Object . create ( null ), ordinal :++ generationOrdinal , failedExports : 0 };
2026-10-14 00:37:32 +02:00
const agentFactory = async ( protocol , signal )=>{ const Base = protocol === 'https:' ? https . Agent : http . Agent ; class PrivateAgent extends Base { addRequest ( request , options ){ if ( ! current . active ){ const error = Error ( 'Diagnostics cancelled' ); error . code = 'SAFETYNET_DIAGNOSTICS_CANCELLED' ; request . destroy ( error ); return ;} const attempt = current . attempts [ signal ]; if ( attempt ){ current . requests . set ( request , attempt ); request . once ( 'close' ,()=> current . requests . delete ( request )); request . once ( 'response' , response =>{ if ( Number . isInteger ( response . statusCode ) && response . statusCode >= 100 && response . statusCode <= 599 ) attempt . httpStatus = response . statusCode ;}); request . once ( 'error' , error =>{ if ([ 'SAFETYNET_DIAGNOSTICS_CANCELLED' , 'ETIMEDOUT' , 'ECONNREFUSED' , 'ECONNRESET' ]. includes ( error . code )) attempt . nativeCode = error . code ;});} for ( const key of request . getHeaderNames ()) if ( ! [ 'host' , 'content-type' , 'content-length' , 'user-agent' ]. includes ( key . toLowerCase ())) request . removeHeader ( key ); super . addRequest ( request , options );}}
2026-10-13 10:33:31 +02:00
const agent = new PrivateAgent ({ keepAlive : false , maxSockets : 1 , rejectUnauthorized : true }); current . agents . add ( agent ); return agent ;};
2026-10-14 00:37:32 +02:00
const base = config . endpoint . replace ( /\/$/ , '' ); const exporterConfig = ( path , signal )=>({ url : base + path , headers : {}, timeoutMillis : config . exportTimeoutMs , concurrencyLimit : 1 , compression : 'none' , httpAgentOptions : protocol => agentFactory ( protocol , signal )});
2026-10-13 23:19:22 +02:00
const { TracerProvider , BatchSpanProcessor , TraceIdRatioBasedSampler , ParentBasedSampler , LoggerProvider , BatchLogRecordProcessor , resourceFromAttributes , OTLPTraceExporter , OTLPLogExporter } = loadTelemetryModules ();
2026-10-13 10:33:31 +02:00
const resource = resourceFromAttributes ({ 'service.name' : 'safetynet-browser' , 'service.version' : '0.1.0' , 'safetynet.diagnostics.mode' : config . mode });
const limits = { maxQueueSize : config . maxQueueSize , maxExportBatchSize : Math . min ( config . maxQueueSize , 128 ), scheduledDelayMillis : config . flushIntervalMs , exportTimeoutMillis : config . exportTimeoutMs };
2026-10-14 00:37:32 +02:00
current . tracerProvider = new TracerProvider ({ resource , sampler : new ParentBasedSampler ({ root : new TraceIdRatioBasedSampler ( config . mode === 'debug' ? config . debugSampleRate : config . sampleRate )}), spanLimits : { attributeCountLimit : 20 , attributeValueLengthLimit : 64 , eventCountLimit : 0 , linkCountLimit : 0 }, spanProcessors : [ new BatchSpanProcessor ({... limits , exporter : gatedExporter ( new OTLPTraceExporter ( exporterConfig ( '/v1/traces' , 'traces' )), current , 'traces' )})]});
current . loggerProvider = new LoggerProvider ({ resource , logRecordLimits : { attributeCountLimit : 20 , attributeValueLengthLimit : 64 }, processors : [ new BatchLogRecordProcessor ({... limits , exporter : gatedExporter ( new OTLPLogExporter ( exporterConfig ( '/v1/logs' , 'logs' )), current , 'logs' )})]}); current . tracer = current . tracerProvider . getTracer ( 'safetynet.browser' , '0.1.0' ); current . logger = current . loggerProvider . getLogger ( 'safetynet.browser' , '0.1.0' ); generation = current ; return true ;
2026-10-13 10:33:31 +02:00
} catch { generation = null ; retire ( current ); return false ;}
}
function event ( name , input = {}){ const current = generation ; if ( ! current ? . active ||! names . has ( name )) return ; try { const parent = storage . getStore (), rate = current . config . mode === 'debug' ? current . config . debugSampleRate : current . config . sampleRate ; if ( parent ? . generation === current ){ if ( ! parent . span . isRecording ()) return ;} else if ( randomInt ( 1000000 ) >= Math . floor ( rate * 1000000 )) return ;
2026-10-14 01:18:23 +02:00
current . logger . emit ({ severityNumber : 9 , severityText : 'INFO' , body : name , attributes : {...( parent ? . generation === current ? parent . context || current . context : current . context ),... attributes ( input )},...( parent ? . generation === current ? { context : trace . setSpan ( ROOT_CONTEXT , parent . span )} : {})});
2026-10-13 10:33:31 +02:00
} catch {}
}
2026-10-14 01:18:23 +02:00
async function span ( name , input , fn ){ if ( typeof fn !== 'function' ) throw TypeError ( 'Diagnostics operation requires a function' ); const current = generation ; if ( ! current ? . active ||! names . has ( name ) || activeSpans >= 128 ) return fn (); let nativeSpan , operationContext ; try { const parent = storage . getStore (); operationContext = parent ? . generation === current ? parent . context || current . context : current . context ; const context = parent ? . generation === current ? trace . setSpan ( ROOT_CONTEXT , parent . span ) : ROOT_CONTEXT ; nativeSpan = current . tracer . startSpan ( name ,{ attributes : {... operationContext ,... attributes ( input )}}, context );} catch { return fn ();}
activeSpans ++ ; const started = performance . now (); let status = 'ok' ; try { const result = await storage . run ({ generation : current , span : nativeSpan , context : operationContext }, fn ); try { nativeSpan . setStatus ({ code : SpanStatusCode . OK });} catch {} return result ;} catch ( error ){ status = 'error' ; try { nativeSpan . setStatus ({ code : SpanStatusCode . ERROR }); nativeSpan . setAttribute ( 'error.type' , error instanceof TypeError ? 'TypeError' : error instanceof RangeError ? 'RangeError' : 'Error' );} catch {} throw error ;} finally { activeSpans -- ; try { const sampled = nativeSpan . isRecording (); nativeSpan . setAttribute ( 'safetynet.status' , status ); nativeSpan . setAttribute ( 'safetynet.duration.ms' , Math . max ( 0 , performance . now () - started )); nativeSpan . end (); if ( current . active && sampled ) current . logger . emit ({ severityNumber : 9 , severityText : 'INFO' , body : name , attributes : {... operationContext ,... attributes ( input ), 'safetynet.status' : status , 'safetynet.duration.ms' : Math . max ( 0 , performance . now () - started )}, context : trace . setSpan ( ROOT_CONTEXT , nativeSpan )});} catch {}}
2026-10-13 10:33:31 +02:00
}
2026-10-13 19:41:27 +02:00
// Synchronous persistence must retain its native return/throw contract. Never
// wrap fsync or DPAPI in a promise merely to add diagnostic timing.
2026-10-14 01:18:23 +02:00
function spanSync ( name , input , fn ){ if ( typeof fn !== 'function' ) throw TypeError ( 'Diagnostics operation requires a function' ); const current = generation ; if ( ! current ? . active ||! names . has ( name ) || activeSpans >= 128 ) return fn (); let nativeSpan , operationContext ; try { const parent = storage . getStore (); operationContext = parent ? . generation === current ? parent . context || current . context : current . context ; const context = parent ? . generation === current ? trace . setSpan ( ROOT_CONTEXT , parent . span ) : ROOT_CONTEXT ; nativeSpan = current . tracer . startSpan ( name ,{ attributes : {... operationContext ,... attributes ( input )}}, context );} catch { return fn ();}
activeSpans ++ ; const started = performance . now (); let status = 'ok' ; try { const result = storage . run ({ generation : current , span : nativeSpan , context : operationContext }, fn ); try { nativeSpan . setStatus ({ code : SpanStatusCode . OK });} catch {} return result ;} catch ( error ){ status = 'error' ; try { nativeSpan . setStatus ({ code : SpanStatusCode . ERROR }); nativeSpan . setAttribute ( 'error.type' , error instanceof TypeError ? 'TypeError' : error instanceof RangeError ? 'RangeError' : 'Error' );} catch {} throw error ;} finally { activeSpans -- ; try { const sampled = nativeSpan . isRecording (); nativeSpan . setAttribute ( 'safetynet.status' , status ); nativeSpan . setAttribute ( 'safetynet.duration.ms' , Math . max ( 0 , performance . now () - started )); nativeSpan . end (); if ( current . active && sampled ) current . logger . emit ({ severityNumber : 9 , severityText : 'INFO' , body : name , attributes : {... operationContext ,... attributes ( input ), 'safetynet.status' : status , 'safetynet.duration.ms' : Math . max ( 0 , performance . now () - started )}, context : trace . setSpan ( ROOT_CONTEXT , nativeSpan )});} catch {}}
2026-10-13 19:41:27 +02:00
}
2026-10-13 18:24:44 +02:00
// Durable events may be delivered by a flusher started under another event's
// async context. Adopt only data-property IDs; never inherit that unrelated
// parent, invoke caller getters, or allow diagnostic failure to repeat work.
function withCorrelation ( ids , fn ){
if ( typeof fn !== 'function' ) throw TypeError ( 'Diagnostics operation requires a function' );
const current = generation ; let parent = null ;
try {
if ( current ? . active && ids && typeof ids === 'object' &&! Array . isArray ( ids ) && [ Object . prototype , null ]. includes ( Object . getPrototypeOf ( ids ))){
const descriptors = Object . getOwnPropertyDescriptors ( ids ), traceId = descriptors . traceId ? . value , spanId = descriptors . spanId ? . value ;
if ( typeof traceId === 'string' && /^[a-f0-9]{32}$/ . test ( traceId ) && traceId !== '0' . repeat ( 32 ) && typeof spanId === 'string' && /^[a-f0-9]{16}$/ . test ( spanId ) && spanId !== '0' . repeat ( 16 )){
const rate = current . config . mode === 'debug' ? current . config . debugSampleRate : current . config . sampleRate ;
const sampled = randomInt ( 1000000 ) < Math . floor ( rate * 1000000 );
2026-10-14 01:18:23 +02:00
parent = { generation : current , context : current . context , span : trace . wrapSpanContext ({ traceId , spanId , traceFlags : sampled ? TraceFlags . SAMPLED : TraceFlags . NONE , isRemote : false })};
2026-10-13 18:24:44 +02:00
}
}
} catch { /* Invalid diagnostic metadata cannot affect protected delivery. */ }
return storage . run ( parent , fn );
}
2026-10-14 01:18:23 +02:00
function flush (){ const current = generation ; if ( ! current ? . active ) return Promise . resolve (); if ( current . flushPromise ) return current . flushPromise ; const pending = Promise . allSettled ([ current . tracerProvider . forceFlush ({ timeoutMillis : current . config . exportTimeoutMs }), current . loggerProvider . forceFlush ({ timeoutMillis : current . config . exportTimeoutMs })]); current . flushPromise = pending ; pending . finally (()=>{ if ( current . flushPromise === pending ) current . flushPromise = null ;}); return pending ;}
2026-10-13 10:33:31 +02:00
async function shutdown (){ if ( closed ) return ; await flush (); closed = true ; const current = generation ; generation = null ; retire ( current ); await Promise . allSettled ([... retiring ]);}
2026-10-14 01:18:23 +02:00
return { configure , canContinue , span , spanSync , event , importStages , withCorrelation , flush , shutdown , correlation : ()=>{ const parent = storage . getStore (); if ( ! parent || parent . generation !== generation ||! generation ? . active ||! parent . span . isRecording ()) return {}; const ids = parent . span . spanContext (); return { traceId : ids . traceId , spanId : ids . spanId };}, status : ()=>({ enabled :!! generation ? . active , mode : generation ? . config . mode || 'operational' , failedExports : generation ? . failedExports || 0 , lifetimeFailedExports , activeSpans , failureReasons : {... failureReasons }, failureFacts : failureFacts . map ( fact =>({... fact }))})};
2026-10-13 10:33:31 +02:00
}
module . exports = { createDiagnostics , attributes , endpointAllowed };