|  | 
|  | 1 | +import { defineIntegration } from '@sentry/core'; | 
|  | 2 | +import type { Event, Exception, IntegrationFn } from '@sentry/types'; | 
|  | 3 | +import { LRUMap, logger } from '@sentry/utils'; | 
|  | 4 | +import { Worker } from 'worker_threads'; | 
|  | 5 | + | 
|  | 6 | +import type { NodeClient } from '../../sdk/client'; | 
|  | 7 | +import type { FrameVariables, LocalVariablesIntegrationOptions, LocalVariablesWorkerArgs } from './common'; | 
|  | 8 | +import { functionNamesMatch, hashFrames } from './common'; | 
|  | 9 | + | 
|  | 10 | +// This string is a placeholder that gets overwritten with the worker code. | 
|  | 11 | +export const base64WorkerScript = '###LocalVariablesWorkerScript###'; | 
|  | 12 | + | 
|  | 13 | +function log(...args: unknown[]): void { | 
|  | 14 | +  logger.log('[LocalVariables]', ...args); | 
|  | 15 | +} | 
|  | 16 | + | 
|  | 17 | +/** | 
|  | 18 | + * Adds local variables to exception frames | 
|  | 19 | + */ | 
|  | 20 | +export const localVariablesAsyncIntegration = defineIntegration((( | 
|  | 21 | +  integrationOptions: LocalVariablesIntegrationOptions = {}, | 
|  | 22 | +) => { | 
|  | 23 | +  const cachedFrames: LRUMap<string, FrameVariables[]> = new LRUMap(20); | 
|  | 24 | + | 
|  | 25 | +  function addLocalVariablesToException(exception: Exception): void { | 
|  | 26 | +    const hash = hashFrames(exception?.stacktrace?.frames); | 
|  | 27 | + | 
|  | 28 | +    if (hash === undefined) { | 
|  | 29 | +      return; | 
|  | 30 | +    } | 
|  | 31 | + | 
|  | 32 | +    // Check if we have local variables for an exception that matches the hash | 
|  | 33 | +    // remove is identical to get but also removes the entry from the cache | 
|  | 34 | +    const cachedFrame = cachedFrames.remove(hash); | 
|  | 35 | + | 
|  | 36 | +    if (cachedFrame === undefined) { | 
|  | 37 | +      return; | 
|  | 38 | +    } | 
|  | 39 | + | 
|  | 40 | +    // Filter out frames where the function name is `new Promise` since these are in the error.stack frames | 
|  | 41 | +    // but do not appear in the debugger call frames | 
|  | 42 | +    const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); | 
|  | 43 | + | 
|  | 44 | +    for (let i = 0; i < frames.length; i++) { | 
|  | 45 | +      // Sentry frames are in reverse order | 
|  | 46 | +      const frameIndex = frames.length - i - 1; | 
|  | 47 | + | 
|  | 48 | +      // Drop out if we run out of frames to match up | 
|  | 49 | +      if (!frames[frameIndex] || !cachedFrame[i]) { | 
|  | 50 | +        break; | 
|  | 51 | +      } | 
|  | 52 | + | 
|  | 53 | +      if ( | 
|  | 54 | +        // We need to have vars to add | 
|  | 55 | +        cachedFrame[i].vars === undefined || | 
|  | 56 | +        // We're not interested in frames that are not in_app because the vars are not relevant | 
|  | 57 | +        frames[frameIndex].in_app === false || | 
|  | 58 | +        // The function names need to match | 
|  | 59 | +        !functionNamesMatch(frames[frameIndex].function, cachedFrame[i].function) | 
|  | 60 | +      ) { | 
|  | 61 | +        continue; | 
|  | 62 | +      } | 
|  | 63 | + | 
|  | 64 | +      frames[frameIndex].vars = cachedFrame[i].vars; | 
|  | 65 | +    } | 
|  | 66 | +  } | 
|  | 67 | + | 
|  | 68 | +  function addLocalVariablesToEvent(event: Event): Event { | 
|  | 69 | +    for (const exception of event.exception?.values || []) { | 
|  | 70 | +      addLocalVariablesToException(exception); | 
|  | 71 | +    } | 
|  | 72 | + | 
|  | 73 | +    return event; | 
|  | 74 | +  } | 
|  | 75 | + | 
|  | 76 | +  async function startInspector(): Promise<void> { | 
|  | 77 | +    // We load inspector dynamically because on some platforms Node is built without inspector support | 
|  | 78 | +    const inspector = await import('inspector'); | 
|  | 79 | +    if (!inspector.url()) { | 
|  | 80 | +      inspector.open(0); | 
|  | 81 | +    } | 
|  | 82 | +  } | 
|  | 83 | + | 
|  | 84 | +  function startWorker(options: LocalVariablesWorkerArgs): void { | 
|  | 85 | +    const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { | 
|  | 86 | +      workerData: options, | 
|  | 87 | +    }); | 
|  | 88 | + | 
|  | 89 | +    process.on('exit', () => { | 
|  | 90 | +      // eslint-disable-next-line @typescript-eslint/no-floating-promises | 
|  | 91 | +      worker.terminate(); | 
|  | 92 | +    }); | 
|  | 93 | + | 
|  | 94 | +    worker.on('message', ({ exceptionHash, frames }) => { | 
|  | 95 | +      cachedFrames.set(exceptionHash, frames); | 
|  | 96 | +    }); | 
|  | 97 | + | 
|  | 98 | +    worker.once('error', (err: Error) => { | 
|  | 99 | +      log('Worker error', err); | 
|  | 100 | +    }); | 
|  | 101 | + | 
|  | 102 | +    worker.once('exit', (code: number) => { | 
|  | 103 | +      log('Worker exit', code); | 
|  | 104 | +    }); | 
|  | 105 | + | 
|  | 106 | +    // Ensure this thread can't block app exit | 
|  | 107 | +    worker.unref(); | 
|  | 108 | +  } | 
|  | 109 | + | 
|  | 110 | +  return { | 
|  | 111 | +    name: 'LocalVariablesAsync', | 
|  | 112 | +    setup(client: NodeClient) { | 
|  | 113 | +      const clientOptions = client.getOptions(); | 
|  | 114 | + | 
|  | 115 | +      if (!clientOptions.includeLocalVariables) { | 
|  | 116 | +        return; | 
|  | 117 | +      } | 
|  | 118 | + | 
|  | 119 | +      const options: LocalVariablesWorkerArgs = { | 
|  | 120 | +        ...integrationOptions, | 
|  | 121 | +        debug: logger.isEnabled(), | 
|  | 122 | +      }; | 
|  | 123 | + | 
|  | 124 | +      startInspector().then( | 
|  | 125 | +        () => { | 
|  | 126 | +          try { | 
|  | 127 | +            startWorker(options); | 
|  | 128 | +          } catch (e) { | 
|  | 129 | +            logger.error('Failed to start worker', e); | 
|  | 130 | +          } | 
|  | 131 | +        }, | 
|  | 132 | +        e => { | 
|  | 133 | +          logger.error('Failed to start inspector', e); | 
|  | 134 | +        }, | 
|  | 135 | +      ); | 
|  | 136 | +    }, | 
|  | 137 | +    processEvent(event: Event): Event { | 
|  | 138 | +      return addLocalVariablesToEvent(event); | 
|  | 139 | +    }, | 
|  | 140 | +  }; | 
|  | 141 | +}) satisfies IntegrationFn); | 
0 commit comments