Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/apm-integrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,22 @@ jobs:
api_key: ${{ secrets.DD_API_KEY }}
service: dd-trace-js-tests

electron:
runs-on: ubuntu-latest
env:
PLUGINS: electron
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- uses: ./.github/actions/testagent/start
- uses: ./.github/actions/node/latest
- uses: ./.github/actions/install
- run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- run: |
sudo apt-get update
sudo apt-get install -y xvfb
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
- run: DISPLAY=:99 yarn test:plugins:ci

express:
runs-on: ubuntu-latest
env:
Expand Down
2 changes: 2 additions & 0 deletions docs/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
241 changes: 241 additions & 0 deletions packages/datadog-instrumentations/src/electron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
'use strict'

const { join } = require('path')
const { wrap } = require('../../datadog-shimmer')
const { addHook, channel, tracingChannel } = require('./helpers/instrument')

const requestCh = tracingChannel('apm:electron:net:request')
const mainReceiveCh = tracingChannel('apm:electron:ipc:main:receive')
const mainHandleCh = tracingChannel('apm:electron:ipc:main:handle')
const mainSendCh = tracingChannel('apm:electron:ipc:main:send')
const rendererPatchedCh = channel('apm:electron:ipc:renderer:patched')
const rendererReceiveCh = tracingChannel('apm:electron:ipc:renderer:receive')
const rendererSendCh = tracingChannel('apm:electron:ipc:renderer:send')

const listeners = {}
const handlers = {}

function createWrapRequest (ch) {
return function wrapRequest (request) {
return function (...args) {
if (!ch.start.hasSubscribers) return request.apply(this, arguments)

const ctx = { args }

return ch.start.runStores(ctx, () => {
try {
const req = request.apply(this, ctx.args)
const emit = req.emit

ctx.req = req

req.emit = function (eventName, arg) {
/* eslint-disable no-fallthrough */
switch (eventName) {
case 'response':
ctx.res = arg
ctx.res.on('error', error => {
ctx.error = error
ch.error.publish(ctx)
ch.asyncStart.publish(ctx)
})
ctx.res.on('end', () => ch.asyncStart.publish(ctx))
break
case 'error':
ctx.error = arg
ch.error.publish(ctx)
case 'abort':
ch.asyncStart.publish(ctx)
}

return emit.apply(this, arguments)
}

return req
} catch (e) {
ctx.error = e
ch.error.publish(ctx)
throw e
} finally {
ch.end.publish(ctx)
}
})
}
}
}

function createWrapAddListener (ch, mappings) {
return function wrapAddListener (addListener) {
return function (channel, listener) {
const wrappedListener = (event, ...args) => {
const ctx = { args, channel, event }

return ch.tracePromise(() => listener.call(this, event, ...args), ctx)
}

const mapping = mappings[channel] || new WeakMap()
const wrapper = mapping.get(listener) || wrappedListener

mapping.set(listener, wrapper)

return addListener.call(this, channel, wrappedListener)
}
}
}

function createWrapRemoveListener (mappings) {
return function wrapRemoveListener (removeListener) {
return function (channel, listener) {
const mapping = mappings[channel]

if (mapping) {
const wrapper = mapping.get(listener)

if (wrapper) {
return removeListener.call(this, channel, wrapper)
}
}

return removeListener.call(this, channel, listener)
}
}
}

function createWrapRemoveAllListeners (mappings) {
return function wrapRemoveAllListeners (removeAllListeners) {
return function (channel) {
if (channel) {
delete mappings[channel]
} else {
Object.keys(mappings).forEach(key => delete mappings[key])
}

return removeAllListeners.apply(this, channel)
}
}
}

function createWrapSend (ch, promise = false) {
const trace = (promise ? ch.tracePromise : ch.traceSync).bind(ch)

return function wrapSend (send) {
return function (channel, ...args) {
const ctx = { args, channel, self: this }

return trace(() => send.call(this, channel, ...args), ctx)
}
}
}

function wrapSendToFrame (send) {
return function (frameId, channel, ...args) {
const ctx = { args, channel, frameId, self: this }

return mainSendCh.traceSync(() => send.call(this, frameId, channel, ...args), ctx)
}
}

function wrapBrowserWindow (electron) {
const moduleExports = {}

class DatadogBrowserWindow extends electron.BrowserWindow {
constructor (options = {}) {
const win = super(options)

// TODO: Move this to plugin?
win.webContents.session.registerPreloadScript({
type: 'frame', // TODO: service-worker
filePath: join(__dirname, 'electron', 'preload.js')
})

// BrowserWindow doesn't support subclassing because it's all native code
// so we return an instance of it instead of the subclass.
return win
}
}

Object.defineProperty(moduleExports, 'BrowserWindow', {
enumerable: true,
get: () => DatadogBrowserWindow,
configurable: false
})

for (const key of Reflect.ownKeys(electron)) {
const descriptor = Reflect.getOwnPropertyDescriptor(electron, key)

if (key === 'BrowserWindow') continue

Object.defineProperty(moduleExports, key, descriptor)
}

return moduleExports
}

function wrapWebContents (proto) {
const descriptor = Object.getOwnPropertyDescriptor(proto, 'webContents')
const wrapped = new WeakSet()
const wrapSend = createWrapSend(mainSendCh)

Object.defineProperty(proto, 'webContents', {
get () {
const webContents = descriptor.get.apply(this)

if (!wrapped.has(webContents)) {
// wrap(webContents, 'postMessage', wrapSend)
wrap(webContents, 'send', wrapSend)
wrap(webContents, 'sendToFrame', wrapSendToFrame)

wrapped.add(webContents)
}

return webContents
}
})
}

addHook({ name: 'electron', versions: ['>=37.0.0'] }, electron => {
// Electron exports a string in Node and an object in Electron.
if (typeof electron === 'string') return electron

const { BrowserWindow, ipcMain, ipcRenderer, net } = electron

if (net) {
// This also covers `fetch` as it uses `request` under the hood.
wrap(net, 'request', createWrapRequest(requestCh))
}

if (ipcRenderer) {
wrap(ipcRenderer, 'invoke', createWrapSend(rendererSendCh, true))
// wrap(ipcRenderer, 'postMessage', createWrapSend(rendererSendCh))
wrap(ipcRenderer, 'send', createWrapSend(rendererSendCh))
wrap(ipcRenderer, 'sendSync', createWrapSend(rendererSendCh))
wrap(ipcRenderer, 'sendToHost', createWrapSend(rendererSendCh))

wrap(ipcRenderer, 'addListener', createWrapAddListener(rendererReceiveCh, listeners))
wrap(ipcRenderer, 'off', createWrapRemoveListener(listeners))
wrap(ipcRenderer, 'on', createWrapAddListener(rendererReceiveCh, listeners))
wrap(ipcRenderer, 'once', createWrapAddListener(rendererReceiveCh, listeners))
wrap(ipcRenderer, 'removeListener', createWrapRemoveListener(listeners))
wrap(ipcRenderer, 'removeAllListeners', createWrapRemoveAllListeners(listeners))

ipcRenderer.send('datadog:apm:renderer:patched')
} else {
wrap(ipcMain, 'addListener', createWrapAddListener(mainReceiveCh, listeners))
wrap(ipcMain, 'handle', createWrapAddListener(mainHandleCh, handlers))
wrap(ipcMain, 'handleOnce', createWrapAddListener(mainHandleCh, handlers))
wrap(ipcMain, 'off', createWrapRemoveListener(listeners))
wrap(ipcMain, 'on', createWrapAddListener(mainReceiveCh, listeners))
wrap(ipcMain, 'once', createWrapAddListener(mainReceiveCh, listeners))
wrap(ipcMain, 'removeAllListeners', createWrapRemoveAllListeners(listeners))
wrap(ipcMain, 'removeHandler', createWrapRemoveAllListeners(handlers))
wrap(ipcMain, 'removeListener', createWrapRemoveListener(listeners))

ipcMain.once('datadog:apm:renderer:patched', event => rendererPatchedCh.publish(event))

wrapWebContents(BrowserWindow.prototype)

electron = wrapBrowserWindow(electron)
}

return electron
})
5 changes: 5 additions & 0 deletions packages/datadog-instrumentations/src/electron/preload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* eslint-disable unicorn/no-empty-file */

'use strict'

// TODO: Expose main process APIs through `contextBridge`.
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
29 changes: 17 additions & 12 deletions packages/datadog-instrumentations/src/helpers/register.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down
2 changes: 1 addition & 1 deletion packages/datadog-instrumentations/src/jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ function getCliWrapper (isNewJestVersion) {
const timeoutPromise = new Promise((resolve) => {
timeoutId = setTimeout(() => {
resolve('timeout')
}, FLUSH_TIMEOUT).unref()
}, FLUSH_TIMEOUT).unref?.()
})

testSessionFinishCh.publish({
Expand Down
17 changes: 17 additions & 0 deletions packages/datadog-plugin-electron/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict'

const CompositePlugin = require('../../dd-trace/src/plugins/composite')
const ElectronIpcPlugin = require('./ipc')
const ElectronNetPlugin = require('./net')

class ElectronPlugin extends CompositePlugin {
static id = 'electron'
static get plugins () {
return {
net: ElectronNetPlugin,
ipc: ElectronIpcPlugin
}
}
}

module.exports = ElectronPlugin
Loading