diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index 493bcc7cfea..ff4e4e01f91 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -393,6 +393,22 @@ jobs: api_key: ${{ secrets.DD_API_KEY }} service: dd-trace-js-tests + electron: + runs-on: ubuntu-latest + env: + PLUGINS: electron + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/latest + - uses: ./.github/actions/install + - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - run: | + sudo apt-get update + sudo apt-get install -y xvfb + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + - run: DISPLAY=:99 yarn test:plugins:ci + express: runs-on: ubuntu-latest env: diff --git a/docs/test.ts b/docs/test.ts index 5a6edcbb219..a41be5f2240 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -323,6 +323,8 @@ tracer.use('cucumber', { service: 'cucumber-service' }); tracer.use('dns'); tracer.use('elasticsearch'); tracer.use('elasticsearch', elasticsearchOptions); +tracer.use('electron'); +tracer.use('electron', { net: httpClientOptions }); tracer.use('express'); tracer.use('express', httpServerOptions); tracer.use('fastify'); diff --git a/index.d.ts b/index.d.ts index 83e8ded0b4c..558434d74d7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -201,6 +201,7 @@ interface Plugins { "cypress": tracer.plugins.cypress; "dns": tracer.plugins.dns; "elasticsearch": tracer.plugins.elasticsearch; + "electron": tracer.plugins.electron; "express": tracer.plugins.express; "fastify": tracer.plugins.fastify; "fetch": tracer.plugins.fetch; @@ -1814,6 +1815,14 @@ declare namespace tracer { }; } + /** + * This plugin automatically instruments the + * [electron](https://github.com/electron/electron) module. + */ + interface electron extends Instrumentation { + net?: HttpClient + } + /** * This plugin automatically instruments the * [express](http://expressjs.com/) module. diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js new file mode 100644 index 00000000000..335530b1668 --- /dev/null +++ b/packages/datadog-instrumentations/src/electron.js @@ -0,0 +1,241 @@ +'use strict' + +const { join } = require('path') +const { wrap } = require('../../datadog-shimmer') +const { addHook, channel, tracingChannel } = require('./helpers/instrument') + +const requestCh = tracingChannel('apm:electron:net:request') +const mainReceiveCh = tracingChannel('apm:electron:ipc:main:receive') +const mainHandleCh = tracingChannel('apm:electron:ipc:main:handle') +const mainSendCh = tracingChannel('apm:electron:ipc:main:send') +const rendererPatchedCh = channel('apm:electron:ipc:renderer:patched') +const rendererReceiveCh = tracingChannel('apm:electron:ipc:renderer:receive') +const rendererSendCh = tracingChannel('apm:electron:ipc:renderer:send') + +const listeners = {} +const handlers = {} + +function createWrapRequest (ch) { + return function wrapRequest (request) { + return function (...args) { + if (!ch.start.hasSubscribers) return request.apply(this, arguments) + + const ctx = { args } + + return ch.start.runStores(ctx, () => { + try { + const req = request.apply(this, ctx.args) + const emit = req.emit + + ctx.req = req + + req.emit = function (eventName, arg) { + /* eslint-disable no-fallthrough */ + switch (eventName) { + case 'response': + ctx.res = arg + ctx.res.on('error', error => { + ctx.error = error + ch.error.publish(ctx) + ch.asyncStart.publish(ctx) + }) + ctx.res.on('end', () => ch.asyncStart.publish(ctx)) + break + case 'error': + ctx.error = arg + ch.error.publish(ctx) + case 'abort': + ch.asyncStart.publish(ctx) + } + + return emit.apply(this, arguments) + } + + return req + } catch (e) { + ctx.error = e + ch.error.publish(ctx) + throw e + } finally { + ch.end.publish(ctx) + } + }) + } + } +} + +function createWrapAddListener (ch, mappings) { + return function wrapAddListener (addListener) { + return function (channel, listener) { + const wrappedListener = (event, ...args) => { + const ctx = { args, channel, event } + + return ch.tracePromise(() => listener.call(this, event, ...args), ctx) + } + + const mapping = mappings[channel] || new WeakMap() + const wrapper = mapping.get(listener) || wrappedListener + + mapping.set(listener, wrapper) + + return addListener.call(this, channel, wrappedListener) + } + } +} + +function createWrapRemoveListener (mappings) { + return function wrapRemoveListener (removeListener) { + return function (channel, listener) { + const mapping = mappings[channel] + + if (mapping) { + const wrapper = mapping.get(listener) + + if (wrapper) { + return removeListener.call(this, channel, wrapper) + } + } + + return removeListener.call(this, channel, listener) + } + } +} + +function createWrapRemoveAllListeners (mappings) { + return function wrapRemoveAllListeners (removeAllListeners) { + return function (channel) { + if (channel) { + delete mappings[channel] + } else { + Object.keys(mappings).forEach(key => delete mappings[key]) + } + + return removeAllListeners.apply(this, channel) + } + } +} + +function createWrapSend (ch, promise = false) { + const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch) + + return function wrapSend (send) { + return function (channel, ...args) { + const ctx = { args, channel, self: this } + + return trace(() => send.call(this, channel, ...args), ctx) + } + } +} + +function wrapSendToFrame (send) { + return function (frameId, channel, ...args) { + const ctx = { args, channel, frameId, self: this } + + return mainSendCh.traceSync(() => send.call(this, frameId, channel, ...args), ctx) + } +} + +function wrapBrowserWindow (electron) { + const moduleExports = {} + + class DatadogBrowserWindow extends electron.BrowserWindow { + constructor (options = {}) { + const win = super(options) + + // TODO: Move this to plugin? + win.webContents.session.registerPreloadScript({ + type: 'frame', // TODO: service-worker + filePath: join(__dirname, 'electron', 'preload.js') + }) + + // BrowserWindow doesn't support subclassing because it's all native code + // so we return an instance of it instead of the subclass. + return win + } + } + + Object.defineProperty(moduleExports, 'BrowserWindow', { + enumerable: true, + get: () => DatadogBrowserWindow, + configurable: false + }) + + for (const key of Reflect.ownKeys(electron)) { + const descriptor = Reflect.getOwnPropertyDescriptor(electron, key) + + if (key === 'BrowserWindow') continue + + Object.defineProperty(moduleExports, key, descriptor) + } + + return moduleExports +} + +function wrapWebContents (proto) { + const descriptor = Object.getOwnPropertyDescriptor(proto, 'webContents') + const wrapped = new WeakSet() + const wrapSend = createWrapSend(mainSendCh) + + Object.defineProperty(proto, 'webContents', { + get () { + const webContents = descriptor.get.apply(this) + + if (!wrapped.has(webContents)) { + // wrap(webContents, 'postMessage', wrapSend) + wrap(webContents, 'send', wrapSend) + wrap(webContents, 'sendToFrame', wrapSendToFrame) + + wrapped.add(webContents) + } + + return webContents + } + }) +} + +addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { + // Electron exports a string in Node and an object in Electron. + if (typeof electron === 'string') return electron + + const { BrowserWindow, ipcMain, ipcRenderer, net } = electron + + if (net) { + // This also covers `fetch` as it uses `request` under the hood. + wrap(net, 'request', createWrapRequest(requestCh)) + } + + if (ipcRenderer) { + wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true)) + // wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh)) + wrap(ipcRenderer, 'sendToHost', createWrapSend(rendererSendCh)) + + wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners)) + wrap(ipcRenderer, 'on', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners)) + wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) + wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + + ipcRenderer.send('datadog:apm:renderer:patched') + } else { + wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers)) + wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers)) + wrap(ipcMain, 'off', createWrapRemoveListener(listeners)) + wrap(ipcMain, 'on', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'once', createWrapAddListener(mainReceiveCh, listeners)) + wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) + wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) + + ipcMain.once('datadog:apm:renderer:patched', event => rendererPatchedCh.publish(event)) + + wrapWebContents(BrowserWindow.prototype) + + electron = wrapBrowserWindow(electron) + } + + return electron +}) diff --git a/packages/datadog-instrumentations/src/electron/preload.js b/packages/datadog-instrumentations/src/electron/preload.js new file mode 100644 index 00000000000..e99b6519382 --- /dev/null +++ b/packages/datadog-instrumentations/src/electron/preload.js @@ -0,0 +1,5 @@ +/* eslint-disable unicorn/no-empty-file */ + +'use strict' + +// TODO: Expose main process APIs through `contextBridge`. diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index c46827b3537..3b2ef166ac2 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -52,6 +52,7 @@ module.exports = { 'dd-trace-api': () => require('../dd-trace-api'), dns: () => require('../dns'), elasticsearch: () => require('../elasticsearch'), + electron: () => require('../electron'), express: () => require('../express'), 'express-mongo-sanitize': () => require('../express-mongo-sanitize'), 'express-session': () => require('../express-session'), diff --git a/packages/datadog-instrumentations/src/helpers/register.js b/packages/datadog-instrumentations/src/helpers/register.js index 6e5f0f3985a..21bc4556c9a 100644 --- a/packages/datadog-instrumentations/src/helpers/register.js +++ b/packages/datadog-instrumentations/src/helpers/register.js @@ -135,18 +135,23 @@ for (const packageName of names) { } try { - loadChannel.publish({ name, version, file }) - // Send the name and version of the module back to the callback because now addHook - // takes in an array of names so by passing the name the callback will know which module name is being used - // TODO(BridgeAR): This is only true in case the name is identical - // in all loads. If they deviate, the deviating name would not be - // picked up due to the unification. Check what modules actually use the name. - // TODO(BridgeAR): Only replace moduleExports if the hook returns a new value. - // This allows to reduce the instrumentation code (no return needed). - - moduleExports = hook(moduleExports, version, name, isIitm) ?? moduleExports - // Set the moduleExports in the hooks WeakSet - hook[HOOK_SYMBOL].add(moduleExports) + // Electron exports a string in Node which is not supported by + // WeakSets. + if (typeof moduleExports !== 'string') { + loadChannel.publish({ name, version, file }) + // Send the name and version of the module back to the callback + // because now addHook takes in an array of names so by passing + // the name the callback will know which module name is being used + // TODO(BridgeAR): This is only true in case the name is identical + // in all loads. If they deviate, the deviating name would not be + // picked up due to the unification. Check what modules actually use the name. + // TODO(BridgeAR): Only replace moduleExports if the hook returns a new value. + // This allows to reduce the instrumentation code (no return needed). + + moduleExports = hook(moduleExports, version, name, isIitm) ?? moduleExports + // Set the moduleExports in the hooks WeakSet + hook[HOOK_SYMBOL].add(moduleExports) + } } catch (e) { log.info('Error during ddtrace instrumentation of application, aborting.', e) telemetry('error', [ diff --git a/packages/datadog-instrumentations/src/jest.js b/packages/datadog-instrumentations/src/jest.js index 95db563d24d..1f0ca91f45d 100644 --- a/packages/datadog-instrumentations/src/jest.js +++ b/packages/datadog-instrumentations/src/jest.js @@ -883,7 +883,7 @@ function getCliWrapper (isNewJestVersion) { const timeoutPromise = new Promise((resolve) => { timeoutId = setTimeout(() => { resolve('timeout') - }, FLUSH_TIMEOUT).unref() + }, FLUSH_TIMEOUT).unref?.() }) testSessionFinishCh.publish({ diff --git a/packages/datadog-plugin-electron/src/index.js b/packages/datadog-plugin-electron/src/index.js new file mode 100644 index 00000000000..df720d480cf --- /dev/null +++ b/packages/datadog-plugin-electron/src/index.js @@ -0,0 +1,17 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ElectronIpcPlugin = require('./ipc') +const ElectronNetPlugin = require('./net') + +class ElectronPlugin extends CompositePlugin { + static id = 'electron' + static get plugins () { + return { + net: ElectronNetPlugin, + ipc: ElectronIpcPlugin + } + } +} + +module.exports = ElectronPlugin diff --git a/packages/datadog-plugin-electron/src/ipc.js b/packages/datadog-plugin-electron/src/ipc.js new file mode 100644 index 00000000000..b0f1ec8b226 --- /dev/null +++ b/packages/datadog-plugin-electron/src/ipc.js @@ -0,0 +1,137 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const ConsumerPlugin = require('../../dd-trace/src/plugins/consumer') +const ProducerPlugin = require('../../dd-trace/src/plugins/producer') + +class ElectronIpcPlugin extends CompositePlugin { + static id = 'electron:ipc' + static get plugins () { + return { + main: ElectronMainPlugin, + renderer: ElectronRendererPlugin + } + } +} + +class ElectronMainPlugin extends CompositePlugin { + static id = 'electron:ipc:main' + static get plugins () { + return { + receive: ElectronMainReceivePlugin, + send: ElectronMainSendPlugin + } + } +} + +class ElectronRendererPlugin extends CompositePlugin { + static id = 'electron:ipc:renderer' + static get plugins () { + return { + receive: ElectronRendererReceivePlugin, + send: ElectronRendererSendPlugin + } + } +} + +class ElectronRendererReceivePlugin extends ConsumerPlugin { + static id = 'electron:ipc:renderer:receive' + static component = 'electron' + static operation = 'receive' + static prefix = 'tracing:apm:electron:ipc:renderer:receive' + + bindStart (ctx) { + const { args, channel } = ctx + + if (channel?.startsWith('datadog:')) return + + const childOf = this._tracer.extract('text_map', args[args.length - 1]) + + if (childOf) { + args.pop() + } + + this.startSpan({ + childOf, + resource: channel, + type: 'worker', + meta: {} + }, ctx) + + return ctx.currentStore + } + + asyncEnd (ctx) { + this.finish(ctx) + } +} + +class ElectronRendererSendPlugin extends ProducerPlugin { + static id = 'electron:ipc:renderer:send' + static component = 'electron' + static operation = 'send' + static prefix = 'tracing:apm:electron:ipc:renderer:send' + + bindStart (ctx) { + const { args, channel } = ctx + + if (channel?.startsWith('datadog:')) return + + const span = this.startSpan({ + resource: channel, + meta: {} + }, ctx) + + if (this._shouldInject(ctx)) { + const carrier = {} + + this._tracer.inject(span, 'text_map', carrier) + + args.push(carrier) + } + + return ctx.currentStore + } + + end (ctx) { + if (ctx.hasOwnProperty('result')) { + this.finish(ctx) + } + } + + asyncEnd (ctx) { + this.finish(ctx) + } + + // Renderer can always inject since main is guaranteed to be patched. + _shouldInject () { + return true + } +} + +class ElectronMainReceivePlugin extends ElectronRendererReceivePlugin { + static id = 'electron:ipc:main:receive' + static prefix = 'tracing:apm:electron:ipc:main:receive' +} + +class ElectronMainSendPlugin extends ElectronRendererSendPlugin { + static id = 'electron:ipc:main:send' + static prefix = 'tracing:apm:electron:ipc:main:send' + + constructor (...args) { + super(...args) + + this._renderers = new WeakSet() + + this.addSub('apm:electron:ipc:renderer:patched', event => { + this._renderers.add(event.sender) + }) + } + + // Only inject when the renderer was patched. + _shouldInject ({ self }) { + return this._renderers.has(self) + } +} + +module.exports = ElectronIpcPlugin diff --git a/packages/datadog-plugin-electron/src/net.js b/packages/datadog-plugin-electron/src/net.js new file mode 100644 index 00000000000..1a7af2f38a8 --- /dev/null +++ b/packages/datadog-plugin-electron/src/net.js @@ -0,0 +1,82 @@ +'use strict' + +const HttpClientPlugin = require('../../datadog-plugin-http/src/client') +const CompositePlugin = require('../../dd-trace/src/plugins/composite') + +class ElectronNetPlugin extends CompositePlugin { + static id = 'electron:net' + static get plugins () { + return { + request: ElectronRequestPlugin + } + } +} + +class ElectronRequestPlugin extends HttpClientPlugin { + static id = 'electron:net:request' + static component = 'electron' + static operation = 'request' + static prefix = 'tracing:apm:electron:net:request' + + bindStart (ctx) { + const args = ctx.args + + let options = args[0] + + if (typeof options === 'string') { + options = args[0] = { url: options } + } else if (!options) { + options = args[0] = {} + } + + const headers = options.headers || {} + + try { + if (typeof options === 'string') { + options = new URL(options) + } else if (options.url) { + options = new URL(options.url) + } + } catch { + // leave options as-is + } + + options.headers = headers + ctx.args = { options } + + const store = super.bindStart(ctx) + + ctx.args = args + + for (const name in options.headers) { + if (!headers[name]) { + args[0].headers ??= {} + args[0].headers[name] = options.headers[name] + } + } + + return store + } + + asyncStart (ctx) { + const reqHeaders = {} + const resHeaders = {} + const responseHead = ctx.res?._responseHead + const { statusCode } = responseHead || {} + + for (const header in ctx.req._urlLoaderOptions?.headers || {}) { + reqHeaders[header.name] = header.value + } + + for (const header in responseHead?.rawHeaders || {}) { + resHeaders[header.name] = header.value + } + + ctx.req = { headers: reqHeaders } + ctx.res = { headers: resHeaders, statusCode } + + this.finish(ctx) + } +} + +module.exports = ElectronNetPlugin diff --git a/packages/datadog-plugin-electron/test/app/index.html b/packages/datadog-plugin-electron/test/app/index.html new file mode 100644 index 00000000000..1411fe6ba72 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/index.html @@ -0,0 +1,12 @@ + + + + + + + Hello World! + + + + + diff --git a/packages/datadog-plugin-electron/test/app/main.js b/packages/datadog-plugin-electron/test/app/main.js new file mode 100644 index 00000000000..4705e11c760 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -0,0 +1,78 @@ +'use strict' + +/* eslint-disable no-console */ + +const { BrowserWindow, app, ipcMain, net } = require('electron/main') +const { join } = require('path') + +app.on('ready', () => { + process.send('ready') + process.on('message', msg => { + try { + switch (msg.name) { + case 'quit': return app.quit() + case 'fetch': return onFetch(msg) + case 'request': return onRequest(msg) + case 'send': return onSend(msg) + case 'receive': return onReceive(msg) + } + } catch (e) { + console.error(e) + } + }) + + ipcMain.on('datadog:log', (_event, level, ...args) => { + console.log('datadog:log') + console[level](...args) + }) +}) + +function onFetch ({ url }) { + net.fetch(url) +} + +function onRequest ({ options }) { + const req = net.request(options) + + req.on('error', e => console.error(e)) + req.on('response', res => { + res.on('data', () => {}) + }) + + req.end() +} + +function onSend () { + loadWindow(win => { + win.webContents.send('update-counter', 1) + }) +} + +function onReceive () { + const listener = () => { + ipcMain.off('set-title', listener) + } + + ipcMain.on('set-title', listener) + + loadWindow(win => { + win.webContents.send('datadog:test:send') + }) +} + +function loadWindow (onShow) { + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + preload: join(__dirname, 'preload.js') + } + }) + + ipcMain.on('datadog:test:log', (_event, ...args) => { + console.log(...args) + }) + + mainWindow.loadFile('index.html') + mainWindow.once('ready-to-show', () => onShow?.(mainWindow)) +} diff --git a/packages/datadog-plugin-electron/test/app/preload.js b/packages/datadog-plugin-electron/test/app/preload.js new file mode 100644 index 00000000000..89220f3b0b6 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -0,0 +1,20 @@ +'use strict' + +const { ipcRenderer } = require('electron/renderer') + +if (globalThis.logger) { + globalThis.logger.debug = (...args) => ipcRenderer.send('datadog:log', 'debug', ...args) + globalThis.logger.info = (...args) => ipcRenderer.send('datadog:log', 'info', ...args) + globalThis.logger.warn = (...args) => ipcRenderer.send('datadog:log', 'warn', ...args) + globalThis.logger.error = (...args) => ipcRenderer.send('datadog:log', 'error', ...args) +} + +function updateCounter () { + ipcRenderer.off('update-counter', updateCounter) +} + +ipcRenderer.on('update-counter', updateCounter) + +ipcRenderer.on('datadog:test:send', () => { + setImmediate(() => ipcRenderer.send('set-title', 'Test')) +}) diff --git a/packages/datadog-plugin-electron/test/app/renderer.js b/packages/datadog-plugin-electron/test/app/renderer.js new file mode 100644 index 00000000000..8dbfc9d685b --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/renderer.js @@ -0,0 +1,3 @@ +'use strict' + +// TODO: Test automatic injection of APM<->RUM bridge. diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js new file mode 100644 index 00000000000..b4f20d9352e --- /dev/null +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -0,0 +1,195 @@ +'use strict' + +const assert = require('assert') +const proc = require('child_process') +const http = require('http') +const { afterEach, beforeEach, describe, it } = require('mocha') +const { join } = require('path') +const agent = require('../../dd-trace/test/plugins/agent') +const { withVersions } = require('../../dd-trace/test/setup/mocha') + +describe('Plugin', () => { + let child + let listener + let port + + before(done => { + const server = http.createServer((req, res) => { + res.writeHead(200) + res.end() + }) + + listener = server.listen(0, '127.0.0.1', () => { + port = listener.address().port + done() + }) + }) + + after(done => { + listener.close(done) + }) + + withVersions('electron', ['electron'], version => { + const startApp = done => { + const electron = require(`../../../versions/electron@${version}`).get() + + child = proc.spawn(electron, [join(__dirname, 'app', 'main')], { + env: { + ...process.env, + NODE_OPTIONS: `-r ${join(__dirname, 'tracer')}`, + DD_TRACE_AGENT_PORT: agent.port + }, + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + windowsHide: true + }) + + child.on('error', done) + child.on('message', msg => msg === 'ready' && done()) + } + + describe('electron', () => { + describe('without configuration', () => { + beforeEach(() => agent.load('electron')) + beforeEach(done => startApp(done)) + + afterEach(() => agent.close({ ritmReset: false })) + afterEach(done => { + child.send({ name: 'quit' }) + child.on('close', () => done()) + }) + + it('should do automatic instrumentation for fetch', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.name, 'http.request') + assert.strictEqual(span.resource, 'GET') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'client') + assert.strictEqual(meta['http.url'], `http://127.0.0.1:${port}/`) + assert.strictEqual(meta['http.method'], 'GET') + assert.strictEqual(meta['http.status_code'], '200') + }) + .then(done) + .catch(done) + + child.send({ name: 'fetch', url: `http://127.0.0.1:${port}` }) + }) + + it('should do automatic instrumentation for request', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'http') + assert.strictEqual(span.name, 'http.request') + assert.strictEqual(span.resource, 'GET') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'client') + assert.strictEqual(meta['http.url'], `http://127.0.0.1:${port}/`) + assert.strictEqual(meta['http.method'], 'GET') + assert.strictEqual(meta['http.status_code'], '200') + }) + .then(done) + .catch(done) + + child.send({ name: 'request', options: `http://127.0.0.1:${port}/` }) + }) + + it('should do automatic instrumentation for main IPC when receiving', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'worker') + assert.strictEqual(span.name, 'electron.main.receive') + assert.strictEqual(span.resource, 'set-title') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.parent_id, span.trace_id) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'receive' }) + }) + + it('should do automatic instrumentation for main IPC when sending', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.name, 'electron.main.send') + assert.strictEqual(span.resource, 'update-counter') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'producer') + }) + .then(done) + .catch(done) + + child.send({ name: 'send' }) + }) + + it('should do automatic instrumentation for renderer IPC when receiving', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.type, 'worker') + assert.strictEqual(span.name, 'electron.renderer.receive') + assert.strictEqual(span.resource, 'update-counter') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + assert.strictEqual(span.parent_id, span.trace_id) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'send' }) + }) + + it('should do automatic instrumentation for renderer IPC when sending', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span + + assert.strictEqual(span.name, 'electron.renderer.send') + assert.strictEqual(span.resource, 'set-title') + assert.strictEqual(span.service, 'test') + assert.strictEqual(span.error, 0) + + assert.strictEqual(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'producer') + }) + .then(done) + .catch(done) + + child.send({ name: 'receive' }) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-electron/test/tracer.js b/packages/datadog-plugin-electron/test/tracer.js new file mode 100644 index 00000000000..b2e3cdf8f44 --- /dev/null +++ b/packages/datadog-plugin-electron/test/tracer.js @@ -0,0 +1,20 @@ +'use strict' + +/* eslint-disable no-console */ + +const logger = globalThis.logger = { + debug: (...args) => console.debug(...args), + info: (...args) => console.info(...args), + warn: (...args) => console.warn(...args), + error: (...args) => console.error(...args), +} + +require('../../dd-trace') + .init({ + service: 'test', + env: 'tester', + logger, + flushInterval: 0, + plugins: false + }) + .use('electron', true) diff --git a/packages/datadog-plugin-http/src/client.js b/packages/datadog-plugin-http/src/client.js index 12e7b5020da..193b5be1034 100644 --- a/packages/datadog-plugin-http/src/client.js +++ b/packages/datadog-plugin-http/src/client.js @@ -38,9 +38,9 @@ class HttpClientPlugin extends ClientPlugin { // TODO delegate to super.startspan const span = this.startSpan(this.operationName(), { childOf, - integrationName: this.constructor.id, + integrationName: this.component, meta: { - [COMPONENT]: this.constructor.id, + [COMPONENT]: this.component, 'span.kind': 'client', 'service.name': this.serviceName({ pluginConfig: this.config, sessionDetails: extractSessionDetails(options) }), 'resource.name': method, diff --git a/packages/datadog-plugin-openai/src/services.js b/packages/datadog-plugin-openai/src/services.js index 1a07b10a3e2..b1bec40ac39 100644 --- a/packages/datadog-plugin-openai/src/services.js +++ b/packages/datadog-plugin-openai/src/services.js @@ -35,7 +35,7 @@ module.exports.init = function (tracerConfig) { interval = setInterval(() => { metrics.flush() - }, FLUSH_INTERVAL).unref() + }, FLUSH_INTERVAL).unref?.() return { metrics, logger } } diff --git a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js index 277797f9400..f2037e45aef 100644 --- a/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +++ b/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js @@ -213,8 +213,8 @@ let enableEsmRewriter = function (telemetryVerbosity) { } }) - port1.unref() - port2.unref() + port1.unref?.() + port2.unref?.() try { Module.register('./rewriter-esm.mjs', { diff --git a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js index 73ac442b3c4..6e5bf954772 100644 --- a/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +++ b/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js @@ -100,7 +100,7 @@ function clearCache () { // only for test purposes function startClearCacheTimer () { resetVulnerabilityCacheTimer = setInterval(clearCache, RESET_VULNERABILITY_CACHE_INTERVAL) - resetVulnerabilityCacheTimer.unref() + resetVulnerabilityCacheTimer.unref?.() } function stopClearCacheTimer () { diff --git a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js index e7f98765e54..4557a686633 100644 --- a/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +++ b/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js @@ -114,7 +114,7 @@ class TestVisDynamicInstrumentation { }) // Allow the parent to exit even if the worker is still running - this.worker.unref() + this.worker.unref?.() this.breakpointSetChannel.port2.on('message', (probeId) => { const resolve = probeIdToResolveBreakpointSet.get(probeId) @@ -122,7 +122,7 @@ class TestVisDynamicInstrumentation { resolve() probeIdToResolveBreakpointSet.delete(probeId) } - }).unref() + }).unref?.() this.breakpointHitChannel.port2.on('message', ({ snapshot }) => { const { probe: { id: probeId } } = snapshot @@ -132,7 +132,7 @@ class TestVisDynamicInstrumentation { } else { log.warn('Received a breakpoint hit for an unknown probe') } - }).unref() + }).unref?.() this.breakpointRemoveChannel.port2.on('message', (probeId) => { const resolve = probeIdToResolveBreakpointRemove.get(probeId) @@ -140,7 +140,7 @@ class TestVisDynamicInstrumentation { resolve() probeIdToResolveBreakpointRemove.delete(probeId) } - }).unref() + }).unref?.() } } diff --git a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js index 474a1114644..c5f09409c5a 100644 --- a/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +++ b/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js @@ -47,11 +47,11 @@ class CiVisibilityExporter extends AgentInfoExporter { const gitUploadTimeoutId = setTimeout(() => { this._resolveGit(new Error('Timeout while uploading git metadata')) - }, GIT_UPLOAD_TIMEOUT).unref() + }, GIT_UPLOAD_TIMEOUT).unref?.() const canUseCiVisProtocolTimeoutId = setTimeout(() => { this._resolveCanUseCiVisProtocol(false) - }, CAN_USE_CI_VIS_PROTOCOL_TIMEOUT).unref() + }, CAN_USE_CI_VIS_PROTOCOL_TIMEOUT).unref?.() this._gitUploadPromise = new Promise(resolve => { this._resolveGit = (err) => { diff --git a/packages/dd-trace/src/datastreams/processor.js b/packages/dd-trace/src/datastreams/processor.js index f3c59bbe232..0e7569f0e6a 100644 --- a/packages/dd-trace/src/datastreams/processor.js +++ b/packages/dd-trace/src/datastreams/processor.js @@ -154,7 +154,7 @@ class DataStreamsProcessor { if (this.enabled) { this.timer = setInterval(this.onInterval.bind(this), flushInterval) - this.timer.unref() + this.timer.unref?.() } process.once('beforeExit', () => this.onInterval()) } diff --git a/packages/dd-trace/src/debugger/devtools_client/source-maps.js b/packages/dd-trace/src/debugger/devtools_client/source-maps.js index c2294ac8f73..1d935e72815 100644 --- a/packages/dd-trace/src/debugger/devtools_client/source-maps.js +++ b/packages/dd-trace/src/debugger/devtools_client/source-maps.js @@ -54,7 +54,7 @@ function setCacheTTL () { // Clear cache a few seconds after it was last used cache.clear() } - }, 5000).unref() + }, 5000).unref?.() } function loadInlineSourceMap (data) { diff --git a/packages/dd-trace/src/debugger/devtools_client/state.js b/packages/dd-trace/src/debugger/devtools_client/state.js index 4c83a1cf2cc..61679ad4307 100644 --- a/packages/dd-trace/src/debugger/devtools_client/state.js +++ b/packages/dd-trace/src/debugger/devtools_client/state.js @@ -173,7 +173,7 @@ session.on('Debugger.scriptParsed', ({ params }) => { if (reEvaluateProbesTimer === null) { reEvaluateProbesTimer = setTimeout(() => { session.emit('scriptLoadingStabilized') - }, 500).unref() + }, 500).unref?.() } else { reEvaluateProbesTimer.refresh() } diff --git a/packages/dd-trace/src/debugger/index.js b/packages/dd-trace/src/debugger/index.js index 955243e55ad..d19aab32eac 100644 --- a/packages/dd-trace/src/debugger/index.js +++ b/packages/dd-trace/src/debugger/index.js @@ -99,13 +99,13 @@ function start (config, rc) { } }) - worker.unref() - probeChannel.port1.unref() - probeChannel.port2.unref() - logChannel.port1.unref() - logChannel.port2.unref() - configChannel.port1.unref() - configChannel.port2.unref() + worker.unref?.() + probeChannel.port1.unref?.() + probeChannel.port2.unref?.() + logChannel.port1.unref?.() + logChannel.port2.unref?.() + configChannel.port1.unref?.() + configChannel.port2.unref?.() } function configure (config) { diff --git a/packages/dd-trace/src/dogstatsd.js b/packages/dd-trace/src/dogstatsd.js index b33fcfde271..3be3979d0e9 100644 --- a/packages/dd-trace/src/dogstatsd.js +++ b/packages/dd-trace/src/dogstatsd.js @@ -152,7 +152,7 @@ class DogStatsDClient { const socket = dgram.createSocket(type) socket.on('error', () => {}) - socket.unref() + socket.unref?.() return socket } @@ -357,7 +357,7 @@ class CustomMetrics { const flush = this.flush.bind(this) // TODO(bengl) this magic number should be configurable - setInterval(flush, 10 * 1000).unref() + setInterval(flush, 10 * 1000).unref?.() process.once('beforeExit', flush) } diff --git a/packages/dd-trace/src/exporters/agent/index.js b/packages/dd-trace/src/exporters/agent/index.js index 4e6d852c29a..b8f1c7f6e6f 100644 --- a/packages/dd-trace/src/exporters/agent/index.js +++ b/packages/dd-trace/src/exporters/agent/index.js @@ -57,7 +57,7 @@ class AgentExporter { this.#timer = setTimeout(() => { this._writer.flush() this.#timer = undefined - }, flushInterval).unref() + }, flushInterval).unref?.() } } diff --git a/packages/dd-trace/src/exporters/common/agent-info-exporter.js b/packages/dd-trace/src/exporters/common/agent-info-exporter.js index b36a582156f..6ff3e942784 100644 --- a/packages/dd-trace/src/exporters/common/agent-info-exporter.js +++ b/packages/dd-trace/src/exporters/common/agent-info-exporter.js @@ -66,7 +66,7 @@ class AgentInfoExporter { this[timerKey] = setTimeout(() => { writer.flush() this[timerKey] = undefined - }, flushInterval).unref() + }, flushInterval).unref?.() } } diff --git a/packages/dd-trace/src/external-logger/src/index.js b/packages/dd-trace/src/external-logger/src/index.js index 77f5523648a..b0bc5e5707c 100644 --- a/packages/dd-trace/src/external-logger/src/index.js +++ b/packages/dd-trace/src/external-logger/src/index.js @@ -27,7 +27,7 @@ class ExternalLogger { } this.timer = setInterval(() => { this.flush() - }, this.interval).unref() + }, this.interval).unref?.() tracerLogger.debug(`started log writer to https://${this.intake}${this.endpoint}`) } diff --git a/packages/dd-trace/src/llmobs/writers/base.js b/packages/dd-trace/src/llmobs/writers/base.js index 8f4e84b6e61..56a83f283ac 100644 --- a/packages/dd-trace/src/llmobs/writers/base.js +++ b/packages/dd-trace/src/llmobs/writers/base.js @@ -32,7 +32,7 @@ class BaseLLMObsWriter { this._periodic = setInterval(() => { this.flush() - }, this._interval).unref() + }, this._interval).unref?.() this._beforeExitHandler = () => { this.destroy() diff --git a/packages/dd-trace/src/openfeature/writers/base.js b/packages/dd-trace/src/openfeature/writers/base.js index 462280bf5d8..f523166ca66 100644 --- a/packages/dd-trace/src/openfeature/writers/base.js +++ b/packages/dd-trace/src/openfeature/writers/base.js @@ -54,7 +54,7 @@ class BaseFFEWriter { this._periodic = setInterval(() => { this.flush() - }, this._interval).unref() + }, this._interval).unref?.() this._beforeExitHandler = () => { this.destroy() diff --git a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js index 8abea441ad5..7386cb1d48a 100644 --- a/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +++ b/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js @@ -142,7 +142,7 @@ class PeriodicMetricReader { this.#timer = setInterval(() => { this.#collectAndExport() - }, this.#exportInterval).unref() + }, this.#exportInterval).unref?.() } /** diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 091fb24f9ed..7af5de9456f 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -43,6 +43,7 @@ module.exports = { get dns () { return require('../../../datadog-plugin-dns/src') }, get 'dd-trace-api' () { return require('../../../datadog-plugin-dd-trace-api/src') }, get elasticsearch () { return require('../../../datadog-plugin-elasticsearch/src') }, + get electron () { return require('../../../datadog-plugin-electron/src') }, get express () { return require('../../../datadog-plugin-express/src') }, get fastify () { return require('../../../datadog-plugin-fastify/src') }, get 'find-my-way' () { return require('../../../datadog-plugin-find-my-way/src') }, diff --git a/packages/dd-trace/src/profiling/profiler.js b/packages/dd-trace/src/profiling/profiler.js index 8cdb55556be..aac8f816ed4 100644 --- a/packages/dd-trace/src/profiling/profiler.js +++ b/packages/dd-trace/src/profiling/profiler.js @@ -215,7 +215,7 @@ class Profiler extends EventEmitter { this.#lastStart = start if (!this.#timer || timeout !== this._timeoutInterval) { this.#timer = setTimeout(() => this._collect(snapshotKinds.PERIODIC), timeout) - this.#timer.unref() + this.#timer.unref?.() } else { this.#timer.refresh() } diff --git a/packages/dd-trace/src/profiling/profilers/wall.js b/packages/dd-trace/src/profiling/profilers/wall.js index 859d5686867..aafb4b15cb5 100644 --- a/packages/dd-trace/src/profiling/profilers/wall.js +++ b/packages/dd-trace/src/profiling/profilers/wall.js @@ -219,7 +219,7 @@ class NativeWallProfiler { asyncContextsLiveGauge.mark(totalAsyncContextCount) asyncContextsUsedGauge.mark(usedAsyncContextCount) }, this.#telemetryHeartbeatIntervalMillis) - this._contextCountGaugeUpdater.unref() + this._contextCountGaugeUpdater.unref?.() } #enter () { diff --git a/packages/dd-trace/src/profiling/ssi-heuristics.js b/packages/dd-trace/src/profiling/ssi-heuristics.js index 994cf7d6a46..598eb5c5aa9 100644 --- a/packages/dd-trace/src/profiling/ssi-heuristics.js +++ b/packages/dd-trace/src/profiling/ssi-heuristics.js @@ -36,7 +36,7 @@ class SSIHeuristics { setTimeout(() => { this.shortLived = false this._maybeTriggered() - }, this.longLivedThreshold).unref() + }, this.longLivedThreshold).unref?.() this._onSpanCreated = this._onSpanCreated.bind(this) dc.subscribe('dd-trace:span:start', this._onSpanCreated) diff --git a/packages/dd-trace/src/remote_config/scheduler.js b/packages/dd-trace/src/remote_config/scheduler.js index 65460a1e4e3..1bb365e1d4a 100644 --- a/packages/dd-trace/src/remote_config/scheduler.js +++ b/packages/dd-trace/src/remote_config/scheduler.js @@ -17,7 +17,7 @@ class Scheduler { runAfterDelay (interval = this._interval) { this._timer = setTimeout(this._callback, interval, () => this.runAfterDelay()) - this._timer.unref && this._timer.unref() + this._timer.unref && this._timer.unref?.() } stop () { diff --git a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js index fc9cb546adc..107911c2aa0 100644 --- a/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +++ b/packages/dd-trace/src/runtime_metrics/runtime_metrics.js @@ -92,7 +92,7 @@ module.exports = { }, INTERVAL) } - interval.unref() + interval.unref?.() }, stop () { diff --git a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js index 37b0a437c3a..e1f7a281bfc 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js @@ -24,6 +24,14 @@ const messaging = { opName: () => 'azure.servicebus.send', serviceName: ({ tracerService }) => `${tracerService}-azure-service-bus` }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService + }, + 'electron:ipc:renderer:send': { + opName: () => 'electron.renderer.send', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'pubsub.request', serviceName: ({ tracerService }) => `${tracerService}-pubsub` @@ -58,6 +66,14 @@ const messaging = { opName: () => 'amqp.receive', serviceName: amqpServiceName }, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService + }, + 'electron:ipc:renderer:receive': { + opName: () => 'electron.renderer.receive', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'pubsub.receive', serviceName: identityService diff --git a/packages/dd-trace/src/service-naming/schemas/v0/web.js b/packages/dd-trace/src/service-naming/schemas/v0/web.js index 23046f8ce8d..0adf615c74e 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/web.js @@ -35,6 +35,10 @@ const web = { undici: { opName: () => 'undici.request', serviceName: httpPluginClientService + }, + 'electron:net:request': { + opName: () => 'http.request', + serviceName: httpPluginClientService } }, server: { diff --git a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js index 7fa6fd9d72d..5a63ee24834 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js @@ -24,6 +24,14 @@ const messaging = { opName: () => 'azure.eventhubs.send', serviceName: identityService }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService + }, + 'electron:ipc:renderer:send': { + opName: () => 'electron.renderer.send', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.send', serviceName: identityService @@ -49,6 +57,14 @@ const messaging = { consumer: { amqplib: amqpInbound, amqp10: amqpInbound, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService + }, + 'electron:ipc:renderer:receive': { + opName: () => 'electron.renderer.receive', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.process', serviceName: identityService diff --git a/packages/dd-trace/src/service-naming/schemas/v1/web.js b/packages/dd-trace/src/service-naming/schemas/v1/web.js index 66b1afee22f..d625122ecd2 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/web.js @@ -35,6 +35,10 @@ const web = { undici: { opName: () => 'undici.request', serviceName: httpPluginClientService + }, + 'electron:net:request': { + opName: () => 'http.client.request', + serviceName: httpPluginClientService } }, server: { diff --git a/packages/dd-trace/src/span_stats.js b/packages/dd-trace/src/span_stats.js index 1bf0d378a6b..75cba51c9c1 100644 --- a/packages/dd-trace/src/span_stats.js +++ b/packages/dd-trace/src/span_stats.js @@ -149,7 +149,7 @@ class SpanStatsProcessor { if (this.enabled) { this.timer = setInterval(this.onInterval.bind(this), interval * 1e3) - this.timer.unref() + this.timer.unref?.() } } diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 3600de13c1e..bbe85397ed6 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -271,6 +271,7 @@ "DD_TRACE_ELASTIC_ELASTICSEARCH_ENABLED": ["A"], "DD_TRACE_ELASTIC_TRANSPORT_ENABLED": ["A"], "DD_TRACE_ELASTICSEARCH_ENABLED": ["A"], + "DD_TRACE_ELECTRON_ENABLED": ["A"], "DD_TRACE_ENABLED": ["A"], "DD_TRACE_ENCODING_DEBUG": ["A"], "DD_TRACE_EXPERIMENTAL_B3_ENABLED": ["A"], diff --git a/packages/dd-trace/src/telemetry/dependencies.js b/packages/dd-trace/src/telemetry/dependencies.js index 27c2f707921..42b0c9c2e28 100644 --- a/packages/dd-trace/src/telemetry/dependencies.js +++ b/packages/dd-trace/src/telemetry/dependencies.js @@ -75,7 +75,7 @@ function waitAndSend (config, application, host) { if (savedDependenciesToSend.size > 0) { waitAndSend(config, application, host) } - }).unref() + }).unref?.() } function loadAllTheLoadedModules () { diff --git a/packages/dd-trace/src/telemetry/endpoints.js b/packages/dd-trace/src/telemetry/endpoints.js index 60db5004008..a08d1d65637 100644 --- a/packages/dd-trace/src/telemetry/endpoints.js +++ b/packages/dd-trace/src/telemetry/endpoints.js @@ -29,7 +29,7 @@ function endpointKey (method, path) { function scheduleFlush () { if (flushScheduled) return flushScheduled = true - setImmediate(flushAndSend).unref() + setImmediate(flushAndSend).unref?.() } function recordEndpoint (method, path) { diff --git a/packages/dd-trace/src/telemetry/telemetry.js b/packages/dd-trace/src/telemetry/telemetry.js index 55e6f1d6ae6..f39801c3c53 100644 --- a/packages/dd-trace/src/telemetry/telemetry.js +++ b/packages/dd-trace/src/telemetry/telemetry.js @@ -221,7 +221,7 @@ function heartbeat (config, application, host) { const { reqType, payload } = createPayload('app-heartbeat') sendData(config, application, host, reqType, payload, updateRetryData) heartbeat(config, application, host) - }, heartbeatInterval).unref() + }, heartbeatInterval).unref?.() return heartbeatTimeout } @@ -234,7 +234,7 @@ function extendedHeartbeat (config) { } sendData(config, application, host, 'app-extended-heartbeat', payload) Object.keys(extendedHeartbeatPayload).forEach(key => delete extendedHeartbeatPayload[key]) - }, 1000 * 60 * 60 * 24).unref() + }, 1000 * 60 * 60 * 24).unref?.() return extendedInterval } diff --git a/packages/dd-trace/test/plugins/agent.js b/packages/dd-trace/test/plugins/agent.js index 299b2cd63f2..7331bca3b76 100644 --- a/packages/dd-trace/test/plugins/agent.js +++ b/packages/dd-trace/test/plugins/agent.js @@ -487,7 +487,7 @@ module.exports = { const promise = /** @type {Promise} */ (new Promise((resolve, _reject) => { listener = server.listen(0, () => { - const port = listener.address().port + const port = this.port = listener.address().port tracer.init(Object.assign({}, { service: 'test', @@ -671,6 +671,7 @@ module.exports = { return /** @type {Promise} */ (new Promise((resolve, reject) => { this.server.on('close', () => { this.server = null + this.port = null resolve() }) diff --git a/packages/dd-trace/test/plugins/versions/package.json b/packages/dd-trace/test/plugins/versions/package.json index 00b221d3f86..fb417ebd25c 100644 --- a/packages/dd-trace/test/plugins/versions/package.json +++ b/packages/dd-trace/test/plugins/versions/package.json @@ -96,6 +96,7 @@ "dd-trace-api": "1.0.0", "ejs": "3.1.10", "elasticsearch": "16.7.3", + "electron": "39.2.4", "esbuild": "0.27.0", "express": "5.1.0", "express-mongo-sanitize": "2.2.0",