From 6df4aebcd30b1438687fc6de7c1d546688ea3649 Mon Sep 17 00:00:00 2001 From: rochdev Date: Wed, 26 Nov 2025 19:51:52 -0500 Subject: [PATCH 1/2] 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 2/2] 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 }