From 6df4aebcd30b1438687fc6de7c1d546688ea3649 Mon Sep 17 00:00:00 2001 From: rochdev Date: Wed, 26 Nov 2025 19:51:52 -0500 Subject: [PATCH 01/14] add electron net fetch integration --- docs/test.ts | 2 + index.d.ts | 9 ++ .../datadog-instrumentations/src/electron.js | 16 ++++ .../src/helpers/hooks.js | 1 + packages/datadog-plugin-electron/src/index.js | 31 +++++++ packages/datadog-plugin-electron/test/app.js | 12 +++ .../test/index.spec.js | 90 +++++++++++++++++++ .../datadog-plugin-electron/test/tracer.js | 14 +++ packages/datadog-plugin-http/src/client.js | 4 +- packages/dd-trace/src/plugins/index.js | 1 + .../src/service-naming/schemas/v0/web.js | 4 + .../src/service-naming/schemas/v1/web.js | 4 + .../src/supported-configurations.json | 1 + packages/dd-trace/test/plugins/agent.js | 3 +- .../test/plugins/versions/package.json | 1 + 15 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 packages/datadog-instrumentations/src/electron.js create mode 100644 packages/datadog-plugin-electron/src/index.js create mode 100644 packages/datadog-plugin-electron/test/app.js create mode 100644 packages/datadog-plugin-electron/test/index.spec.js create mode 100644 packages/datadog-plugin-electron/test/tracer.js 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..c12cd1a8d5e --- /dev/null +++ b/packages/datadog-instrumentations/src/electron.js @@ -0,0 +1,16 @@ +'use strict' + +const shimmer = require('../../datadog-shimmer') +const { createWrapFetch } = require('./helpers/fetch') +const { addHook, tracingChannel } = require('./helpers/instrument') + +const ch = tracingChannel('apm:electron:net:fetch') + +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 + + shimmer.wrap(electron.net, 'fetch', createWrapFetch(globalThis.Request, ch)) + + return electron +}) 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-plugin-electron/src/index.js b/packages/datadog-plugin-electron/src/index.js new file mode 100644 index 00000000000..23171893a96 --- /dev/null +++ b/packages/datadog-plugin-electron/src/index.js @@ -0,0 +1,31 @@ +'use strict' + +const FetchPlugin = require('../../datadog-plugin-fetch/src') +const CompositePlugin = require('../../dd-trace/src/plugins/composite') + +class ElectronPlugin extends CompositePlugin { + static id = 'electron' + static get plugins () { + return { + net: ElectronNetPlugin + } + } +} + +class ElectronNetPlugin extends CompositePlugin { + static id = 'electron:net' + static get plugins () { + return { + fetch: ElectronFetchPlugin + } + } +} + +class ElectronFetchPlugin extends FetchPlugin { + static id = 'electron:net:fetch' + static component = 'electron' + static operation = 'fetch' + static prefix = 'tracing:apm:electron:net:fetch' +} + +module.exports = ElectronPlugin diff --git a/packages/datadog-plugin-electron/test/app.js b/packages/datadog-plugin-electron/test/app.js new file mode 100644 index 00000000000..30b67b76bf3 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app.js @@ -0,0 +1,12 @@ +'use strict' + +const { app, net } = require('electron') + +const { PORT } = process.env + +app.on('ready', () => { + process.send('ready') + process.on('message', msg => msg === 'quit' && app.quit()) + + net.fetch(`http://127.0.0.1:${PORT}`) +}) 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..52381ab0034 --- /dev/null +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -0,0 +1,90 @@ +'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 + + before(done => { + const server = http.createServer((req, res) => { + res.writeHead(200) + res.end() + }) + + listener = server.listen(0, '127.0.0.1', () => done()) + }) + + after(done => { + listener.close(done) + }) + + withVersions('electron', ['electron'], version => { + const startApp = (port, done) => { + const electron = require(`../../../versions/electron@${version}`).get() + + child = proc.spawn(electron, [join(__dirname, 'app')], { + env: { + ...process.env, + NODE_OPTIONS: `-r ${join(__dirname, 'tracer')}`, + DD_TRACE_AGENT_PORT: port, + PORT: listener.address().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(() => { + return agent.load('electron') + }) + + beforeEach(done => { + startApp(agent.port, done) + }) + + afterEach(() => { + return agent.close({ ritmReset: false }) + }) + + afterEach(done => { + child.send('quit') + child.on('close', () => done()) + }) + + it('should do automatic instrumentation', 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:${listener.address().port}/`) + assert.strictEqual(meta['http.method'], 'GET') + assert.strictEqual(meta['http.status_code'], '200') + }) + .then(done) + .catch(done) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-electron/test/tracer.js b/packages/datadog-plugin-electron/test/tracer.js new file mode 100644 index 00000000000..1654cfdddda --- /dev/null +++ b/packages/datadog-plugin-electron/test/tracer.js @@ -0,0 +1,14 @@ +'use strict' + +const port = process.env.DD_TRACE_AGENT_PORT + +require('../../dd-trace') + .init({ + service: 'test', + env: 'tester', + port, + flushInterval: 0, + plugins: false + }) + .use('electron', true) + .setUrl(`http://127.0.0.1:${port}`) 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/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/service-naming/schemas/v0/web.js b/packages/dd-trace/src/service-naming/schemas/v0/web.js index 23046f8ce8d..2e38c2a641c 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:fetch': { + opName: () => 'http.request', + serviceName: httpPluginClientService } }, server: { 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..cbf85f8a2b9 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:fetch': { + opName: () => 'http.client.request', + serviceName: httpPluginClientService } }, server: { 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/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", From 6613967c63190d2049d97574c8216aa6e8f26604 Mon Sep 17 00:00:00 2001 From: Roch Devost Date: Mon, 1 Dec 2025 18:59:47 -0500 Subject: [PATCH 02/14] add support for net.request --- .../datadog-instrumentations/src/electron.js | 55 +++++++++++++- packages/datadog-plugin-electron/src/index.js | 72 +++++++++++++++++-- packages/datadog-plugin-electron/test/app.js | 33 +++++++-- .../test/index.spec.js | 55 +++++++++----- .../src/service-naming/schemas/v0/web.js | 2 +- .../src/service-naming/schemas/v1/web.js | 2 +- 6 files changed, 187 insertions(+), 32 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index c12cd1a8d5e..2ee44146e22 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -4,13 +4,64 @@ const shimmer = require('../../datadog-shimmer') const { createWrapFetch } = require('./helpers/fetch') const { addHook, tracingChannel } = require('./helpers/instrument') -const ch = tracingChannel('apm:electron:net:fetch') +const fetchCh = tracingChannel('apm:electron:net:fetch') +const requestCh = tracingChannel('apm:electron:net:request') + +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) + } + }) + } + } +} 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 - shimmer.wrap(electron.net, 'fetch', createWrapFetch(globalThis.Request, ch)) + shimmer.wrap(electron.net, 'fetch', createWrapFetch(globalThis.Request, fetchCh)) + shimmer.wrap(electron.net, 'request', createWrapRequest(requestCh)) return electron }) diff --git a/packages/datadog-plugin-electron/src/index.js b/packages/datadog-plugin-electron/src/index.js index 23171893a96..22a340eb340 100644 --- a/packages/datadog-plugin-electron/src/index.js +++ b/packages/datadog-plugin-electron/src/index.js @@ -1,6 +1,6 @@ 'use strict' -const FetchPlugin = require('../../datadog-plugin-fetch/src') +const HttpClientPlugin = require('../../datadog-plugin-http/src/client') const CompositePlugin = require('../../dd-trace/src/plugins/composite') class ElectronPlugin extends CompositePlugin { @@ -16,16 +16,76 @@ class ElectronNetPlugin extends CompositePlugin { static id = 'electron:net' static get plugins () { return { - fetch: ElectronFetchPlugin + request: ElectronRequestPlugin } } } -class ElectronFetchPlugin extends FetchPlugin { - static id = 'electron:net:fetch' +class ElectronRequestPlugin extends HttpClientPlugin { + static id = 'electron:net:request' static component = 'electron' - static operation = 'fetch' - static prefix = 'tracing:apm:electron:net:fetch' + 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 = ElectronPlugin diff --git a/packages/datadog-plugin-electron/test/app.js b/packages/datadog-plugin-electron/test/app.js index 30b67b76bf3..7fe67dab02a 100644 --- a/packages/datadog-plugin-electron/test/app.js +++ b/packages/datadog-plugin-electron/test/app.js @@ -1,12 +1,35 @@ 'use strict' -const { app, net } = require('electron') +/* eslint-disable no-console */ -const { PORT } = process.env +const { app, net } = require('electron') app.on('ready', () => { process.send('ready') - process.on('message', msg => msg === 'quit' && app.quit()) - - net.fetch(`http://127.0.0.1:${PORT}`) + process.on('message', msg => { + try { + switch (msg.name) { + case 'quit': return app.quit() + case 'fetch': return onFetch(msg) + case 'request': return onRequest(msg) + } + } catch (e) { + console.error(e) + } + }) }) + +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() +} diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js index 52381ab0034..9ff66bcdf75 100644 --- a/packages/datadog-plugin-electron/test/index.spec.js +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -11,6 +11,7 @@ const { withVersions } = require('../../dd-trace/test/setup/mocha') describe('Plugin', () => { let child let listener + let port before(done => { const server = http.createServer((req, res) => { @@ -18,7 +19,10 @@ describe('Plugin', () => { res.end() }) - listener = server.listen(0, '127.0.0.1', () => done()) + listener = server.listen(0, '127.0.0.1', () => { + port = listener.address().port + done() + }) }) after(done => { @@ -26,15 +30,14 @@ describe('Plugin', () => { }) withVersions('electron', ['electron'], version => { - const startApp = (port, done) => { + const startApp = done => { const electron = require(`../../../versions/electron@${version}`).get() child = proc.spawn(electron, [join(__dirname, 'app')], { env: { ...process.env, NODE_OPTIONS: `-r ${join(__dirname, 'tracer')}`, - DD_TRACE_AGENT_PORT: port, - PORT: listener.address().port + DD_TRACE_AGENT_PORT: agent.port }, stdio: ['inherit', 'inherit', 'inherit', 'ipc'], windowsHide: true @@ -46,24 +49,40 @@ describe('Plugin', () => { describe('electron', () => { describe('without configuration', () => { - beforeEach(() => { - return agent.load('electron') - }) + beforeEach(() => agent.load('electron')) + beforeEach(done => startApp(done)) - beforeEach(done => { - startApp(agent.port, done) + afterEach(() => agent.close({ ritmReset: false })) + afterEach(done => { + child.send({ name: 'quit' }) + child.on('close', () => done()) }) - afterEach(() => { - return agent.close({ ritmReset: false }) - }) + it('should do automatic instrumentation for fetch', done => { + agent + .assertSomeTraces(traces => { + const span = traces[0][0] + const { meta } = span - afterEach(done => { - child.send('quit') - child.on('close', () => done()) + 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', done => { + it('should do automatic instrumentation for request', done => { agent .assertSomeTraces(traces => { const span = traces[0][0] @@ -77,12 +96,14 @@ describe('Plugin', () => { assert.strictEqual(meta.component, 'electron') assert.strictEqual(meta['span.kind'], 'client') - assert.strictEqual(meta['http.url'], `http://127.0.0.1:${listener.address().port}/`) + 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}/` }) }) }) }) 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 2e38c2a641c..0adf615c74e 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/web.js @@ -36,7 +36,7 @@ const web = { opName: () => 'undici.request', serviceName: httpPluginClientService }, - 'electron:net:fetch': { + 'electron:net:request': { opName: () => 'http.request', serviceName: httpPluginClientService } 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 cbf85f8a2b9..d625122ecd2 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/web.js @@ -36,7 +36,7 @@ const web = { opName: () => 'undici.request', serviceName: httpPluginClientService }, - 'electron:net:fetch': { + 'electron:net:request': { opName: () => 'http.client.request', serviceName: httpPluginClientService } From 10a721e944b389d2fca2aa4f2eedecbd3cf06041 Mon Sep 17 00:00:00 2001 From: rochdev Date: Fri, 5 Dec 2025 14:10:38 -0500 Subject: [PATCH 03/14] code cleanup --- packages/datadog-instrumentations/src/electron.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index 2ee44146e22..039a1b2e65c 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -1,10 +1,8 @@ 'use strict' const shimmer = require('../../datadog-shimmer') -const { createWrapFetch } = require('./helpers/fetch') const { addHook, tracingChannel } = require('./helpers/instrument') -const fetchCh = tracingChannel('apm:electron:net:fetch') const requestCh = tracingChannel('apm:electron:net:request') function createWrapRequest (ch) { @@ -60,7 +58,7 @@ 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 - shimmer.wrap(electron.net, 'fetch', createWrapFetch(globalThis.Request, fetchCh)) + // This also covers `fetch` as it uses `request` under the hood. shimmer.wrap(electron.net, 'request', createWrapRequest(requestCh)) return electron From 77a1bd730d9179a3e8a55a41fd6565ad79e8bf5b Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 16:44:45 -0500 Subject: [PATCH 04/14] add support for ipc --- .../datadog-instrumentations/src/electron.js | 150 ++++++++++++++++- .../src/helpers/register.js | 29 ++-- packages/datadog-plugin-electron/src/index.js | 82 +--------- packages/datadog-plugin-electron/src/ipc.js | 153 ++++++++++++++++++ packages/datadog-plugin-electron/src/net.js | 82 ++++++++++ packages/datadog-plugin-electron/test/app.js | 35 ---- .../test/app/index.html | 12 ++ .../datadog-plugin-electron/test/app/main.js | 62 +++++++ .../test/app/preload.js | 16 ++ .../test/app/renderer.js | 3 + .../test/index.spec.js | 43 ++++- .../datadog-plugin-electron/test/tracer.js | 12 +- .../service-naming/schemas/v0/messaging.js | 8 + .../service-naming/schemas/v1/messaging.js | 8 + 14 files changed, 563 insertions(+), 132 deletions(-) create mode 100644 packages/datadog-plugin-electron/src/ipc.js create mode 100644 packages/datadog-plugin-electron/src/net.js delete mode 100644 packages/datadog-plugin-electron/test/app.js create mode 100644 packages/datadog-plugin-electron/test/app/index.html create mode 100644 packages/datadog-plugin-electron/test/app/main.js create mode 100644 packages/datadog-plugin-electron/test/app/preload.js create mode 100644 packages/datadog-plugin-electron/test/app/renderer.js diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index 039a1b2e65c..7a466de4919 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -1,9 +1,18 @@ 'use strict' const shimmer = require('../../datadog-shimmer') -const { addHook, tracingChannel } = require('./helpers/instrument') +const { addHook, channel, tracingChannel } = require('./helpers/instrument') const requestCh = tracingChannel('apm:electron:net:request') +const mainEmitCh = channel('apm:electron:ipc:main:emit') +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 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) { @@ -54,12 +63,147 @@ function createWrapRequest (ch) { } } +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) { + return function wrapSend (send) { + return function (channel, ...args) { + const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch) + const ctx = { args, channel, self: this } + + return trace(() => send.call(this, channel, ...args), ctx) + } + } +} + +function wrapEmit (emit) { + return function (channel, event, ...args) { + mainEmitCh.publish({ channel, event, args }) + + return emit.apply(this, arguments) + } +} + +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 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)) { + shimmer.wrap(webContents, 'postMessage', wrapSend) + shimmer.wrap(webContents, 'send', wrapSend) + shimmer.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 - // This also covers `fetch` as it uses `request` under the hood. - shimmer.wrap(electron.net, 'request', createWrapRequest(requestCh)) + const { BrowserWindow, ipcMain, ipcRenderer, net } = electron + + if (net) { + // This also covers `fetch` as it uses `request` under the hood. + shimmer.wrap(net, 'request', createWrapRequest(requestCh)) + } + + if (ipcMain) { + shimmer.wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners)) + shimmer.wrap(ipcMain, 'emit', wrapEmit) + shimmer.wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers)) + shimmer.wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers)) + shimmer.wrap(ipcMain, 'off', createWrapRemoveListener(listeners)) + shimmer.wrap(ipcMain, 'on', createWrapAddListener(mainReceiveCh, listeners)) + shimmer.wrap(ipcMain, 'once', createWrapAddListener(mainReceiveCh, listeners)) + shimmer.wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + shimmer.wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) + shimmer.wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) + } + + if (BrowserWindow) { + wrapWebContents(BrowserWindow.prototype) + } + + if (ipcRenderer) { + shimmer.wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true)) + shimmer.wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) + shimmer.wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh)) + shimmer.wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh)) + + shimmer.wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners)) + shimmer.wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners)) + shimmer.wrap(ipcRenderer, 'on', createWrapAddListener(rendererReceiveCh, listeners)) + shimmer.wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners)) + shimmer.wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) + shimmer.wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + } return electron }) 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-plugin-electron/src/index.js b/packages/datadog-plugin-electron/src/index.js index 22a340eb340..df720d480cf 100644 --- a/packages/datadog-plugin-electron/src/index.js +++ b/packages/datadog-plugin-electron/src/index.js @@ -1,91 +1,17 @@ 'use strict' -const HttpClientPlugin = require('../../datadog-plugin-http/src/client') 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 + net: ElectronNetPlugin, + ipc: ElectronIpcPlugin } } } -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 = 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..378c4f592de --- /dev/null +++ b/packages/datadog-plugin-electron/src/ipc.js @@ -0,0 +1,153 @@ +'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 ElectronMainReceivePlugin extends ConsumerPlugin { + static id = 'electron:ipc:main:receive' + static component = 'electron' + static operation = 'receive' + static prefix = 'tracing:apm:electron:ipc:main: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 ElectronMainSendPlugin extends ProducerPlugin { + static id = 'electron:ipc:main:send' + static component = 'electron' + static operation = 'send' + static prefix = 'tracing:apm:electron:ipc:main:send' + + constructor (...args) { + super(...args) + + this._senders = new WeakMap() + + this.addSub('apm:electron:ipc:main:emit', ({ channel, event, args }) => { + if (channel !== 'datadog:apm:full') return + + this._senders.set(event.sender, args[0]) + }) + } + + bindStart (ctx) { + const { args, channel, self } = ctx + + if (channel?.startsWith('datadog:')) return + + const span = this.startSpan({ + resource: channel, + meta: {} + }, ctx) + + if (this._senders.get(self.sender)) { + const carrier = {} + + this._tracer.inject(span, 'text_map', carrier) + + args.push(carrier) + } + + return ctx.currentStore + } + + end (ctx) { + this.finish(ctx) + } +} + +class ElectronRendererReceivePlugin extends ElectronMainReceivePlugin { + static id = 'electron:ipc:renderer:receive' + static prefix = 'tracing:apm:electron:ipc:renderer:receive' +} + +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 carrier = {} + const span = this.startSpan({ + resource: channel, + meta: {} + }, ctx) + + 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) + } +} + +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.js b/packages/datadog-plugin-electron/test/app.js deleted file mode 100644 index 7fe67dab02a..00000000000 --- a/packages/datadog-plugin-electron/test/app.js +++ /dev/null @@ -1,35 +0,0 @@ -'use strict' - -/* eslint-disable no-console */ - -const { app, net } = require('electron') - -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) - } - } catch (e) { - console.error(e) - } - }) -}) - -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() -} 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..743921d12d7 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -0,0 +1,62 @@ +'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 'render': return onRender(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 onRender () { + const listener = event => { + ipcMain.off('set-title', listener) + event.returnValue = 'done' + } + + ipcMain.on('set-title', listener) + + const mainWindow = new BrowserWindow({ + show: false, + webPreferences: { + nodeIntegration: true, + preload: join(__dirname, 'preload.js') + } + }) + + mainWindow.loadFile('index.html') + mainWindow.webContents.send('update-counter', 1) +} 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..4adc684d130 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -0,0 +1,16 @@ +'use strict' + +const { contextBridge, ipcRenderer } = require('electron/renderer') + +ipcRenderer.send('datadog:apm:full', !!globalThis._ddtrace) + +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) +} + +contextBridge.exposeInMainWorld('electronAPI', { + setTitle: title => ipcRenderer.sendSync('set-title', title) +}) 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..9e51ebdb6f1 --- /dev/null +++ b/packages/datadog-plugin-electron/test/app/renderer.js @@ -0,0 +1,3 @@ +'use strict' + +window.electronAPI.setTitle('Test') diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js index 9ff66bcdf75..3cca0b32878 100644 --- a/packages/datadog-plugin-electron/test/index.spec.js +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -33,7 +33,7 @@ describe('Plugin', () => { const startApp = done => { const electron = require(`../../../versions/electron@${version}`).get() - child = proc.spawn(electron, [join(__dirname, 'app')], { + child = proc.spawn(electron, [join(__dirname, 'app', 'main')], { env: { ...process.env, NODE_OPTIONS: `-r ${join(__dirname, 'tracer')}`, @@ -105,6 +105,47 @@ describe('Plugin', () => { 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(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'render' }) + }) + + 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: 'render' }) + }) }) }) }) diff --git a/packages/datadog-plugin-electron/test/tracer.js b/packages/datadog-plugin-electron/test/tracer.js index 1654cfdddda..b2e3cdf8f44 100644 --- a/packages/datadog-plugin-electron/test/tracer.js +++ b/packages/datadog-plugin-electron/test/tracer.js @@ -1,14 +1,20 @@ 'use strict' -const port = process.env.DD_TRACE_AGENT_PORT +/* 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', - port, + logger, flushInterval: 0, plugins: false }) .use('electron', true) - .setUrl(`http://127.0.0.1:${port}`) 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..250cbd3a275 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,10 @@ const messaging = { opName: () => 'azure.servicebus.send', serviceName: ({ tracerService }) => `${tracerService}-azure-service-bus` }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'pubsub.request', serviceName: ({ tracerService }) => `${tracerService}-pubsub` @@ -58,6 +62,10 @@ const messaging = { opName: () => 'amqp.receive', serviceName: amqpServiceName }, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'pubsub.receive', serviceName: identityService 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..cc1bf395b5a 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,10 @@ const messaging = { opName: () => 'azure.eventhubs.send', serviceName: identityService }, + 'electron:ipc:main:send': { + opName: () => 'electron.main.send', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.send', serviceName: identityService @@ -49,6 +53,10 @@ const messaging = { consumer: { amqplib: amqpInbound, amqp10: amqpInbound, + 'electron:ipc:main:receive': { + opName: () => 'electron.main.receive', + serviceName: identityService + }, 'google-cloud-pubsub': { opName: () => 'gcp.pubsub.process', serviceName: identityService From 57a2ebc787664088ff5c902cd475b0228556a1a1 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 16:45:51 -0500 Subject: [PATCH 05/14] add ci --- .github/workflows/apm-integrations.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index 493bcc7cfea..dd57893e1ee 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -393,6 +393,17 @@ 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: yarn test:plugins:ci + express: runs-on: ubuntu-latest env: From b1b43472752ccaa5d18b39a923865d6cf07feb21 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 17:11:29 -0500 Subject: [PATCH 06/14] try to fix weird ubuntu error --- .github/workflows/apm-integrations.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index dd57893e1ee..2617b238125 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -402,6 +402,7 @@ jobs: - 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: yarn test:plugins:ci express: From 3a7d04ff4c42e69e9cfcc3730c7f41c8c2990e04 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 18:52:10 -0500 Subject: [PATCH 07/14] fix renderer failing because of missing unref method --- .../datadog-instrumentations/src/electron.js | 3 +- packages/datadog-instrumentations/src/jest.js | 2 +- .../test/app/preload.js | 6 +++ .../test/index.spec.js | 41 +++++++++++++++++++ .../datadog-plugin-openai/src/services.js | 2 +- .../appsec/iast/taint-tracking/rewriter.js | 4 +- .../src/appsec/iast/vulnerability-reporter.js | 2 +- .../dynamic-instrumentation/index.js | 8 ++-- .../exporters/ci-visibility-exporter.js | 4 +- .../dd-trace/src/datastreams/processor.js | 2 +- .../debugger/devtools_client/source-maps.js | 2 +- .../src/debugger/devtools_client/state.js | 2 +- packages/dd-trace/src/debugger/index.js | 14 +++---- packages/dd-trace/src/dogstatsd.js | 4 +- .../dd-trace/src/exporters/agent/index.js | 2 +- .../exporters/common/agent-info-exporter.js | 2 +- .../dd-trace/src/external-logger/src/index.js | 2 +- packages/dd-trace/src/llmobs/writers/base.js | 2 +- .../dd-trace/src/openfeature/writers/base.js | 2 +- .../metrics/periodic_metric_reader.js | 2 +- packages/dd-trace/src/profiling/profiler.js | 2 +- .../dd-trace/src/profiling/profilers/wall.js | 2 +- .../dd-trace/src/profiling/ssi-heuristics.js | 2 +- .../dd-trace/src/remote_config/scheduler.js | 2 +- .../src/runtime_metrics/runtime_metrics.js | 2 +- .../service-naming/schemas/v0/messaging.js | 8 ++++ .../service-naming/schemas/v1/messaging.js | 8 ++++ packages/dd-trace/src/span_stats.js | 2 +- .../dd-trace/src/telemetry/dependencies.js | 2 +- packages/dd-trace/src/telemetry/endpoints.js | 2 +- packages/dd-trace/src/telemetry/telemetry.js | 4 +- 31 files changed, 104 insertions(+), 40 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index 7a466de4919..c64ccd95a4e 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -115,9 +115,10 @@ function createWrapRemoveAllListeners (mappings) { } function createWrapSend (ch, promise = false) { + const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch) + return function wrapSend (send) { return function (channel, ...args) { - const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch) const ctx = { args, channel, self: this } return trace(() => send.call(this, channel, ...args), ctx) 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/test/app/preload.js b/packages/datadog-plugin-electron/test/app/preload.js index 4adc684d130..b98ddbe7665 100644 --- a/packages/datadog-plugin-electron/test/app/preload.js +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -14,3 +14,9 @@ if (globalThis.logger) { contextBridge.exposeInMainWorld('electronAPI', { setTitle: title => ipcRenderer.sendSync('set-title', title) }) + +const listener = () => { + ipcRenderer.off('update-counter', listener) +} + +ipcRenderer.on('update-counter', listener) diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js index 3cca0b32878..f6af9edfbd3 100644 --- a/packages/datadog-plugin-electron/test/index.spec.js +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -146,6 +146,47 @@ describe('Plugin', () => { child.send({ name: 'render' }) }) + + 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(meta.component, 'electron') + assert.strictEqual(meta['span.kind'], 'consumer') + }) + .then(done) + .catch(done) + + child.send({ name: 'render' }) + }) + + 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: 'render' }) + }) }) }) }) 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/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 250cbd3a275..e1f7a281bfc 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/messaging.js @@ -28,6 +28,10 @@ const messaging = { 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` @@ -66,6 +70,10 @@ const messaging = { 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/v1/messaging.js b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js index cc1bf395b5a..5a63ee24834 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/messaging.js @@ -28,6 +28,10 @@ const messaging = { 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 @@ -57,6 +61,10 @@ const messaging = { 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/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/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 } From 977e01dde1921d9818e101d510321d91c0b21e48 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 19:41:35 -0500 Subject: [PATCH 08/14] fix propagation and add test --- packages/datadog-instrumentations/src/electron.js | 15 +++++---------- packages/datadog-plugin-electron/src/ipc.js | 10 ++++------ packages/datadog-plugin-electron/test/app/main.js | 4 +++- .../datadog-plugin-electron/test/app/preload.js | 2 -- .../datadog-plugin-electron/test/index.spec.js | 2 ++ 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index c64ccd95a4e..abb18e0a595 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -4,7 +4,7 @@ const shimmer = require('../../datadog-shimmer') const { addHook, channel, tracingChannel } = require('./helpers/instrument') const requestCh = tracingChannel('apm:electron:net:request') -const mainEmitCh = channel('apm:electron:ipc:main:emit') +const mainFullCh = channel('apm:electron:ipc:main:full') const mainReceiveCh = tracingChannel('apm:electron:ipc:main:receive') const mainHandleCh = tracingChannel('apm:electron:ipc:main:handle') const mainSendCh = tracingChannel('apm:electron:ipc:main:send') @@ -126,14 +126,6 @@ function createWrapSend (ch, promise = false) { } } -function wrapEmit (emit) { - return function (channel, event, ...args) { - mainEmitCh.publish({ channel, event, args }) - - return emit.apply(this, arguments) - } -} - function wrapSendToFrame (send) { return function (frameId, channel, ...args) { const ctx = { args, channel, frameId, self: this } @@ -177,7 +169,6 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { if (ipcMain) { shimmer.wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners)) - shimmer.wrap(ipcMain, 'emit', wrapEmit) shimmer.wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers)) shimmer.wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers)) shimmer.wrap(ipcMain, 'off', createWrapRemoveListener(listeners)) @@ -186,6 +177,8 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { shimmer.wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) shimmer.wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) shimmer.wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) + + ipcMain.once('datadog:apm:full', event => mainFullCh.publish(event)) } if (BrowserWindow) { @@ -204,6 +197,8 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { shimmer.wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners)) shimmer.wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) shimmer.wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) + + ipcRenderer.send('datadog:apm:full') } return electron diff --git a/packages/datadog-plugin-electron/src/ipc.js b/packages/datadog-plugin-electron/src/ipc.js index 378c4f592de..769d52f7058 100644 --- a/packages/datadog-plugin-electron/src/ipc.js +++ b/packages/datadog-plugin-electron/src/ipc.js @@ -75,12 +75,10 @@ class ElectronMainSendPlugin extends ProducerPlugin { constructor (...args) { super(...args) - this._senders = new WeakMap() + this._senders = new WeakSet() - this.addSub('apm:electron:ipc:main:emit', ({ channel, event, args }) => { - if (channel !== 'datadog:apm:full') return - - this._senders.set(event.sender, args[0]) + this.addSub('apm:electron:ipc:main:full', event => { + this._senders.add(event.sender) }) } @@ -94,7 +92,7 @@ class ElectronMainSendPlugin extends ProducerPlugin { meta: {} }, ctx) - if (this._senders.get(self.sender)) { + if (this._senders.has(self)) { const carrier = {} this._tracer.inject(span, 'text_map', carrier) diff --git a/packages/datadog-plugin-electron/test/app/main.js b/packages/datadog-plugin-electron/test/app/main.js index 743921d12d7..d8b80285a77 100644 --- a/packages/datadog-plugin-electron/test/app/main.js +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -58,5 +58,7 @@ function onRender () { }) mainWindow.loadFile('index.html') - mainWindow.webContents.send('update-counter', 1) + mainWindow.once('ready-to-show', () => { + mainWindow.webContents.send('update-counter', 1) + }) } diff --git a/packages/datadog-plugin-electron/test/app/preload.js b/packages/datadog-plugin-electron/test/app/preload.js index b98ddbe7665..120f8eac94c 100644 --- a/packages/datadog-plugin-electron/test/app/preload.js +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -2,8 +2,6 @@ const { contextBridge, ipcRenderer } = require('electron/renderer') -ipcRenderer.send('datadog:apm:full', !!globalThis._ddtrace) - if (globalThis.logger) { globalThis.logger.debug = (...args) => ipcRenderer.send('datadog:log', 'debug', ...args) globalThis.logger.info = (...args) => ipcRenderer.send('datadog:log', 'info', ...args) diff --git a/packages/datadog-plugin-electron/test/index.spec.js b/packages/datadog-plugin-electron/test/index.spec.js index f6af9edfbd3..22305580919 100644 --- a/packages/datadog-plugin-electron/test/index.spec.js +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -117,6 +117,7 @@ describe('Plugin', () => { 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') @@ -158,6 +159,7 @@ describe('Plugin', () => { 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') From 0cc5e5bf003d2ced8856d74a8e38294e7165d2ad Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 19:43:24 -0500 Subject: [PATCH 09/14] try to fix lack of x display --- .github/workflows/apm-integrations.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index 2617b238125..ef22dd3ee5f 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -403,6 +403,11 @@ jobs: - 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 & + export DISPLAY=:99 - run: yarn test:plugins:ci express: From b2bf282eb147ec70ed4e8d64a6c4de6e2d0fdfdc Mon Sep 17 00:00:00 2001 From: rochdev Date: Sat, 6 Dec 2025 19:45:21 -0500 Subject: [PATCH 10/14] try to fix lack of x display --- .github/workflows/apm-integrations.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/apm-integrations.yml b/.github/workflows/apm-integrations.yml index ef22dd3ee5f..ff4e4e01f91 100644 --- a/.github/workflows/apm-integrations.yml +++ b/.github/workflows/apm-integrations.yml @@ -407,8 +407,7 @@ jobs: sudo apt-get update sudo apt-get install -y xvfb Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - export DISPLAY=:99 - - run: yarn test:plugins:ci + - run: DISPLAY=:99 yarn test:plugins:ci express: runs-on: ubuntu-latest From b5073bb3d8f0937017ccbd9f0e25b1dd34c6e591 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sun, 7 Dec 2025 21:54:36 -0500 Subject: [PATCH 11/14] make test more precise and instrument only send methods --- .../datadog-instrumentations/src/electron.js | 5 ++-- .../datadog-plugin-electron/test/app/main.js | 24 +++++++++++++------ .../test/app/preload.js | 16 ++++++------- .../test/app/renderer.js | 2 +- .../test/index.spec.js | 8 +++---- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index abb18e0a595..193377ec2dd 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -144,7 +144,7 @@ function wrapWebContents (proto) { const webContents = descriptor.get.apply(this) if (!wrapped.has(webContents)) { - shimmer.wrap(webContents, 'postMessage', wrapSend) + // shimmer.wrap(webContents, 'postMessage', wrapSend) shimmer.wrap(webContents, 'send', wrapSend) shimmer.wrap(webContents, 'sendToFrame', wrapSendToFrame) @@ -187,9 +187,10 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { if (ipcRenderer) { shimmer.wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true)) - shimmer.wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) + // shimmer.wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) shimmer.wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh)) shimmer.wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh)) + shimmer.wrap(ipcRenderer, 'sendToHost', createWrapSend(rendererSendCh)) shimmer.wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners)) shimmer.wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners)) diff --git a/packages/datadog-plugin-electron/test/app/main.js b/packages/datadog-plugin-electron/test/app/main.js index d8b80285a77..bdf910bfa96 100644 --- a/packages/datadog-plugin-electron/test/app/main.js +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -13,7 +13,8 @@ app.on('ready', () => { case 'quit': return app.quit() case 'fetch': return onFetch(msg) case 'request': return onRequest(msg) - case 'render': return onRender(msg) + case 'send': return onSend(msg) + case 'receive': return onReceive(msg) } } catch (e) { console.error(e) @@ -41,14 +42,25 @@ function onRequest ({ options }) { req.end() } -function onRender () { - const listener = event => { +function onSend () { + loadWindow(win => { + win.webContents.send('update-counter', 1) + }) +} + +function onReceive () { + const listener = () => { ipcMain.off('set-title', listener) - event.returnValue = 'done' } ipcMain.on('set-title', listener) + loadWindow(win => { + win.webContents.send('datadog:test:send') + }) +} + +function loadWindow (onShow) { const mainWindow = new BrowserWindow({ show: false, webPreferences: { @@ -58,7 +70,5 @@ function onRender () { }) mainWindow.loadFile('index.html') - mainWindow.once('ready-to-show', () => { - mainWindow.webContents.send('update-counter', 1) - }) + 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 index 120f8eac94c..89220f3b0b6 100644 --- a/packages/datadog-plugin-electron/test/app/preload.js +++ b/packages/datadog-plugin-electron/test/app/preload.js @@ -1,6 +1,6 @@ 'use strict' -const { contextBridge, ipcRenderer } = require('electron/renderer') +const { ipcRenderer } = require('electron/renderer') if (globalThis.logger) { globalThis.logger.debug = (...args) => ipcRenderer.send('datadog:log', 'debug', ...args) @@ -9,12 +9,12 @@ if (globalThis.logger) { globalThis.logger.error = (...args) => ipcRenderer.send('datadog:log', 'error', ...args) } -contextBridge.exposeInMainWorld('electronAPI', { - setTitle: title => ipcRenderer.sendSync('set-title', title) -}) - -const listener = () => { - ipcRenderer.off('update-counter', listener) +function updateCounter () { + ipcRenderer.off('update-counter', updateCounter) } -ipcRenderer.on('update-counter', listener) +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 index 9e51ebdb6f1..8dbfc9d685b 100644 --- a/packages/datadog-plugin-electron/test/app/renderer.js +++ b/packages/datadog-plugin-electron/test/app/renderer.js @@ -1,3 +1,3 @@ 'use strict' -window.electronAPI.setTitle('Test') +// 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 index 22305580919..b4f20d9352e 100644 --- a/packages/datadog-plugin-electron/test/index.spec.js +++ b/packages/datadog-plugin-electron/test/index.spec.js @@ -125,7 +125,7 @@ describe('Plugin', () => { .then(done) .catch(done) - child.send({ name: 'render' }) + child.send({ name: 'receive' }) }) it('should do automatic instrumentation for main IPC when sending', done => { @@ -145,7 +145,7 @@ describe('Plugin', () => { .then(done) .catch(done) - child.send({ name: 'render' }) + child.send({ name: 'send' }) }) it('should do automatic instrumentation for renderer IPC when receiving', done => { @@ -167,7 +167,7 @@ describe('Plugin', () => { .then(done) .catch(done) - child.send({ name: 'render' }) + child.send({ name: 'send' }) }) it('should do automatic instrumentation for renderer IPC when sending', done => { @@ -187,7 +187,7 @@ describe('Plugin', () => { .then(done) .catch(done) - child.send({ name: 'render' }) + child.send({ name: 'receive' }) }) }) }) From fba05b8ab657d75a7da671083a4cb52542a76f8b Mon Sep 17 00:00:00 2001 From: rochdev Date: Sun, 7 Dec 2025 22:22:03 -0500 Subject: [PATCH 12/14] code cleanup --- .../datadog-instrumentations/src/electron.js | 6 +- packages/datadog-plugin-electron/src/ipc.js | 82 ++++++++----------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index 193377ec2dd..cda043afdcf 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -4,10 +4,10 @@ const shimmer = require('../../datadog-shimmer') const { addHook, channel, tracingChannel } = require('./helpers/instrument') const requestCh = tracingChannel('apm:electron:net:request') -const mainFullCh = channel('apm:electron:ipc:main:full') 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') @@ -178,7 +178,7 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { shimmer.wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) shimmer.wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) - ipcMain.once('datadog:apm:full', event => mainFullCh.publish(event)) + ipcMain.once('datadog:apm:renderer:patched', event => rendererPatchedCh.publish(event)) } if (BrowserWindow) { @@ -199,7 +199,7 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { shimmer.wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) shimmer.wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) - ipcRenderer.send('datadog:apm:full') + ipcRenderer.send('datadog:apm:renderer:patched') } return electron diff --git a/packages/datadog-plugin-electron/src/ipc.js b/packages/datadog-plugin-electron/src/ipc.js index 769d52f7058..b0f1ec8b226 100644 --- a/packages/datadog-plugin-electron/src/ipc.js +++ b/packages/datadog-plugin-electron/src/ipc.js @@ -34,11 +34,11 @@ class ElectronRendererPlugin extends CompositePlugin { } } -class ElectronMainReceivePlugin extends ConsumerPlugin { - static id = 'electron:ipc:main:receive' +class ElectronRendererReceivePlugin extends ConsumerPlugin { + static id = 'electron:ipc:renderer:receive' static component = 'electron' static operation = 'receive' - static prefix = 'tracing:apm:electron:ipc:main:receive' + static prefix = 'tracing:apm:electron:ipc:renderer:receive' bindStart (ctx) { const { args, channel } = ctx @@ -66,24 +66,14 @@ class ElectronMainReceivePlugin extends ConsumerPlugin { } } -class ElectronMainSendPlugin extends ProducerPlugin { - static id = 'electron:ipc:main:send' +class ElectronRendererSendPlugin extends ProducerPlugin { + static id = 'electron:ipc:renderer:send' static component = 'electron' static operation = 'send' - static prefix = 'tracing:apm:electron:ipc:main:send' - - constructor (...args) { - super(...args) - - this._senders = new WeakSet() - - this.addSub('apm:electron:ipc:main:full', event => { - this._senders.add(event.sender) - }) - } + static prefix = 'tracing:apm:electron:ipc:renderer:send' bindStart (ctx) { - const { args, channel, self } = ctx + const { args, channel } = ctx if (channel?.startsWith('datadog:')) return @@ -92,7 +82,7 @@ class ElectronMainSendPlugin extends ProducerPlugin { meta: {} }, ctx) - if (this._senders.has(self)) { + if (this._shouldInject(ctx)) { const carrier = {} this._tracer.inject(span, 'text_map', carrier) @@ -104,47 +94,43 @@ class ElectronMainSendPlugin extends ProducerPlugin { } end (ctx) { + if (ctx.hasOwnProperty('result')) { + this.finish(ctx) + } + } + + asyncEnd (ctx) { this.finish(ctx) } -} -class ElectronRendererReceivePlugin extends ElectronMainReceivePlugin { - static id = 'electron:ipc:renderer:receive' - static prefix = 'tracing:apm:electron:ipc:renderer:receive' + // Renderer can always inject since main is guaranteed to be patched. + _shouldInject () { + return true + } } -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 carrier = {} - const span = this.startSpan({ - resource: channel, - meta: {} - }, ctx) +class ElectronMainReceivePlugin extends ElectronRendererReceivePlugin { + static id = 'electron:ipc:main:receive' + static prefix = 'tracing:apm:electron:ipc:main:receive' +} - this._tracer.inject(span, 'text_map', carrier) +class ElectronMainSendPlugin extends ElectronRendererSendPlugin { + static id = 'electron:ipc:main:send' + static prefix = 'tracing:apm:electron:ipc:main:send' - args.push(carrier) + constructor (...args) { + super(...args) - return ctx.currentStore - } + this._renderers = new WeakSet() - end (ctx) { - if (ctx.hasOwnProperty('result')) { - this.finish(ctx) - } + this.addSub('apm:electron:ipc:renderer:patched', event => { + this._renderers.add(event.sender) + }) } - asyncEnd (ctx) { - this.finish(ctx) + // Only inject when the renderer was patched. + _shouldInject ({ self }) { + return this._renderers.has(self) } } From 205d3de8f0a2d0d3be4b98717b1a010411217806 Mon Sep 17 00:00:00 2001 From: rochdev Date: Mon, 8 Dec 2025 19:08:36 -0500 Subject: [PATCH 13/14] add preload injection to browser window --- .../datadog-instrumentations/src/electron.js | 115 +++++++++++++----- .../src/electron/preload.js | 5 + .../datadog-plugin-electron/test/app/main.js | 4 + 3 files changed, 91 insertions(+), 33 deletions(-) create mode 100644 packages/datadog-instrumentations/src/electron/preload.js diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index cda043afdcf..ef65ea75ea3 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -1,6 +1,8 @@ 'use strict' -const shimmer = require('../../datadog-shimmer') +const { mkdtempSync, readFileSync, writeFileSync } = require('fs') +const { join } = require('path') +const { wrap } = require('../../datadog-shimmer') const { addHook, channel, tracingChannel } = require('./helpers/instrument') const requestCh = tracingChannel('apm:electron:net:request') @@ -134,6 +136,55 @@ function wrapSendToFrame (send) { } } +function wrapBrowserWindow (electron) { + const moduleExports = {} + + class DatadogBrowserWindow extends electron.BrowserWindow { + constructor (options = {}) { + const webPreferences = options.webPreferences ??= {} + const preload = options.webPreferences.preload + const ddPreload = join(__dirname, 'electron', 'preload.js') + + if (preload) { + const userCode = readFileSync(preload, 'utf8') + const ddCode = ';(() => {' + readFileSync(ddPreload, 'utf8') + '})();' + const useStrict = userCode.match(/['"]use strict['"]/)?.[0] || '' + const tmp = electron.app.getPath('temp') + const dir = mkdtempSync(join(tmp, 'dd-electron-preload-')) + const filename = join(dir, 'preload.js') + + // Preload doesn't support `require` of relative paths in sandboxed mode + // so we merge our preload with the user preload in a single file. + writeFileSync(filename, useStrict + '\n' + ddCode + '\n' + userCode) + + webPreferences.preload = filename + } else { + webPreferences.preload = ddPreload + } + + // BrowserWindow doesn't support subclassing because it's all native code + // so we return an instance of it instead of the subclass. + return super(options) // eslint-disable-line constructor-super + } + } + + 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() @@ -144,9 +195,9 @@ function wrapWebContents (proto) { const webContents = descriptor.get.apply(this) if (!wrapped.has(webContents)) { - // shimmer.wrap(webContents, 'postMessage', wrapSend) - shimmer.wrap(webContents, 'send', wrapSend) - shimmer.wrap(webContents, 'sendToFrame', wrapSendToFrame) + // wrap(webContents, 'postMessage', wrapSend) + wrap(webContents, 'send', wrapSend) + wrap(webContents, 'sendToFrame', wrapSendToFrame) wrapped.add(webContents) } @@ -164,42 +215,40 @@ addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => { if (net) { // This also covers `fetch` as it uses `request` under the hood. - shimmer.wrap(net, 'request', createWrapRequest(requestCh)) + wrap(net, 'request', createWrapRequest(requestCh)) } - if (ipcMain) { - shimmer.wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners)) - shimmer.wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers)) - shimmer.wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers)) - shimmer.wrap(ipcMain, 'off', createWrapRemoveListener(listeners)) - shimmer.wrap(ipcMain, 'on', createWrapAddListener(mainReceiveCh, listeners)) - shimmer.wrap(ipcMain, 'once', createWrapAddListener(mainReceiveCh, listeners)) - shimmer.wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) - shimmer.wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers)) - shimmer.wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners)) + 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)) - } - if (BrowserWindow) { wrapWebContents(BrowserWindow.prototype) - } - if (ipcRenderer) { - shimmer.wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true)) - // shimmer.wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh)) - shimmer.wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh)) - shimmer.wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh)) - shimmer.wrap(ipcRenderer, 'sendToHost', createWrapSend(rendererSendCh)) - - shimmer.wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners)) - shimmer.wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners)) - shimmer.wrap(ipcRenderer, 'on', createWrapAddListener(rendererReceiveCh, listeners)) - shimmer.wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners)) - shimmer.wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners)) - shimmer.wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners)) - - ipcRenderer.send('datadog:apm:renderer:patched') + 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-plugin-electron/test/app/main.js b/packages/datadog-plugin-electron/test/app/main.js index bdf910bfa96..4705e11c760 100644 --- a/packages/datadog-plugin-electron/test/app/main.js +++ b/packages/datadog-plugin-electron/test/app/main.js @@ -69,6 +69,10 @@ function loadWindow (onShow) { } }) + ipcMain.on('datadog:test:log', (_event, ...args) => { + console.log(...args) + }) + mainWindow.loadFile('index.html') mainWindow.once('ready-to-show', () => onShow?.(mainWindow)) } From c95a512c0736d9aeab87c4372eb999a1c28d776f Mon Sep 17 00:00:00 2001 From: rochdev Date: Tue, 9 Dec 2025 20:22:30 -0500 Subject: [PATCH 14/14] register preload script without temp file --- .../datadog-instrumentations/src/electron.js | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/packages/datadog-instrumentations/src/electron.js b/packages/datadog-instrumentations/src/electron.js index ef65ea75ea3..335530b1668 100644 --- a/packages/datadog-instrumentations/src/electron.js +++ b/packages/datadog-instrumentations/src/electron.js @@ -1,6 +1,5 @@ 'use strict' -const { mkdtempSync, readFileSync, writeFileSync } = require('fs') const { join } = require('path') const { wrap } = require('../../datadog-shimmer') const { addHook, channel, tracingChannel } = require('./helpers/instrument') @@ -141,30 +140,17 @@ function wrapBrowserWindow (electron) { class DatadogBrowserWindow extends electron.BrowserWindow { constructor (options = {}) { - const webPreferences = options.webPreferences ??= {} - const preload = options.webPreferences.preload - const ddPreload = join(__dirname, 'electron', 'preload.js') - - if (preload) { - const userCode = readFileSync(preload, 'utf8') - const ddCode = ';(() => {' + readFileSync(ddPreload, 'utf8') + '})();' - const useStrict = userCode.match(/['"]use strict['"]/)?.[0] || '' - const tmp = electron.app.getPath('temp') - const dir = mkdtempSync(join(tmp, 'dd-electron-preload-')) - const filename = join(dir, 'preload.js') - - // Preload doesn't support `require` of relative paths in sandboxed mode - // so we merge our preload with the user preload in a single file. - writeFileSync(filename, useStrict + '\n' + ddCode + '\n' + userCode) - - webPreferences.preload = filename - } else { - webPreferences.preload = ddPreload - } + 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 super(options) // eslint-disable-line constructor-super + return win } }