Skip to content

Commit e3d84c2

Browse files
authored
refactor: Refactored bluebird instrumentation to subscribe to events emitted (#3858)
1 parent 9fdb0b8 commit e3d84c2

File tree

10 files changed

+128
-167
lines changed

10 files changed

+128
-167
lines changed

lib/instrumentation/bluebird.js

Lines changed: 0 additions & 109 deletions
This file was deleted.

lib/instrumentations.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ module.exports = function instrumentations() {
1616
'@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
1717
'@prisma/client': { type: InstrumentationDescriptor.TYPE_DATASTORE },
1818
'aws-sdk': { module: './instrumentation/aws-sdk' },
19-
bluebird: { type: InstrumentationDescriptor.TYPE_PROMISE },
2019
connect: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
2120
fastify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
2221
kafkajs: { type: InstrumentationDescriptor.TYPE_MESSAGE },

lib/subscriber-configs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// 'package-name': [ { path: 'subscriberPath', instrumentations: [] }, ... ]
1010
const subscribers = {
1111
...require('./subscribers/amqplib/config'),
12+
...require('./subscribers/bluebird/config'),
1213
...require('./subscribers/bunyan/config'),
1314
...require('./subscribers/cassandra-driver/config'),
1415
...require('./subscribers/elasticsearch/config'),

lib/subscribers/bluebird/config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
'use strict'
6+
7+
module.exports = {
8+
bluebird: [
9+
{
10+
path: './bluebird/instrumentation',
11+
instrumentations: [{
12+
channelName: 'nr_then',
13+
module: { name: 'bluebird', versionRange: '>=2.02', filePath: 'js/release/promise.js' },
14+
functionQuery: {
15+
functionName: 'Promise',
16+
kind: 'Sync'
17+
}
18+
}]
19+
},
20+
]
21+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2026 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const BaseSubscriber = require('../base')
9+
10+
module.exports = class BluebirdPromiseSubscriber extends BaseSubscriber {
11+
constructor({ agent, logger }) {
12+
super({ agent, logger, channelName: 'nr_then', packageName: 'bluebird' })
13+
}
14+
15+
/**
16+
* We have to wrap the Promise prototype, not just the _then method.
17+
* This is because we need to get the current context from when the promise was constructed.
18+
* If we only wrapped `_then`, the context is not the same, you can see from the versioned tests
19+
* Where it will defer promise resolution
20+
*
21+
* @param {object} data event data
22+
* @param {Context} ctx the current context
23+
* @returns {Context} in this case it is the same context, just with both `_then` and its respective callbacks bound to the current context
24+
*/
25+
handler(data, ctx) {
26+
const { self: BluebirdPromise } = data
27+
const origThen = BluebirdPromise._then
28+
BluebirdPromise._then = this.#wrapThenHandler({ ctx, origThen })
29+
return ctx
30+
}
31+
32+
/**
33+
* wraps the current `_then` method on the instance.
34+
* It binds the context from when promise was constructed to the callbacks(didFulfill, didReject, didProgress)
35+
*
36+
* @param {object} params to function
37+
* @param {Context} params.ctx active context
38+
* @param {Function} params.origThen the original `_then` method on promise instance
39+
* @returns {Function} wrapped `_then` function
40+
*/
41+
#wrapThenHandler({ ctx, origThen }) {
42+
const self = this
43+
return function wrappedThenHandler(...args) {
44+
const [didFulfill, didReject, didProgress] = args
45+
// only bind the callback if the transaction from context is still active
46+
if (ctx.transaction.isActive() === false) {
47+
return origThen.apply(this, args)
48+
}
49+
args[0] = self.agent.tracer.bindFunction(didFulfill, ctx)
50+
args[1] = self.agent.tracer.bindFunction(didReject, ctx)
51+
52+
// didProgress is only applicable to v2.x
53+
if (didProgress) {
54+
args[2] = self.agent.tracer.bindFunction(didProgress, ctx)
55+
}
56+
return self.agent.tracer.bindFunction(origThen, ctx).apply(this, args)
57+
}
58+
}
59+
}

test/versioned/bluebird/baseline.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
'use strict'
77

88
const test = require('node:test')
9+
const assert = require('node:assert')
910

1011
const helper = require('../../lib/agent_helper')
1112
const { version } = require('bluebird/package.json')
@@ -21,9 +22,6 @@ const {
2122
testThrowBehavior,
2223
testTryBehavior,
2324
} = require('./common-tests')
24-
const {
25-
areMethodsWrapped,
26-
} = require('./helpers')
2725

2826
testTryBehavior('try')
2927
testTryBehavior('attempt')
@@ -43,11 +41,18 @@ testAsCallbackBehavior('nodeify')
4341
testCatchBehavior('catch')
4442
testCatchBehavior('caught')
4543

46-
test('bluebird static and instance methods check', function (t) {
44+
test('tracking metrics', async function (t) {
4745
const agent = helper.loadTestAgent(t)
46+
t.after(() => {
47+
helper.unloadAgent(agent)
48+
})
4849
const Promise = require('bluebird')
50+
await helper.runInTransaction(agent, async (tx) => {
51+
assert.ok(tx)
52+
await Promise.resolve()
53+
const ctx = agent.tracer.getContext()
54+
assert.ok(ctx.transaction)
55+
})
4956

50-
areMethodsWrapped(Promise)
51-
areMethodsWrapped(Promise.prototype)
52-
assertPackageMetrics({ agent, pkg: 'bluebird', version })
57+
assertPackageMetrics({ agent, pkg: 'bluebird', version, subscriberType: true })
5358
})

test/versioned/bluebird/common-tests.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ const {
1919
async function testPromiseContext({ t, factory }) {
2020
await t.test('context switch', async function (t) {
2121
const { agent, Promise } = t.nr
22-
factory = factory.bind(null, Promise)
2322
const plan = tspl(t, { plan: 2 })
2423

2524
const ctxA = helper.runInTransaction(agent, function (tx) {
2625
return {
2726
transaction: tx,
28-
promise: factory('[tx a] ')
27+
promise: factory(Promise, '[tx a] ')
2928
}
3029
})
3130

@@ -50,7 +49,6 @@ async function testPromiseContext({ t, factory }) {
5049
await t.test('context loss', async function (t) {
5150
const plan = tspl(t, { plan: 2 })
5251
const { agent, Promise } = t.nr
53-
factory = factory.bind(null, Promise)
5452

5553
const ctxA = helper.runInTransaction(agent, function (tx) {
5654
t.after(function () {
@@ -59,7 +57,7 @@ async function testPromiseContext({ t, factory }) {
5957

6058
return {
6159
transaction: tx,
62-
promise: factory('[tx a] ')
60+
promise: factory(Promise, '[tx a] ')
6361
}
6462
})
6563

@@ -77,9 +75,8 @@ async function testPromiseContext({ t, factory }) {
7775
await t.test('context gain', async function (t) {
7876
const plan = tspl(t, { plan: 2 })
7977
const { agent, Promise } = t.nr
80-
factory = factory.bind(null, Promise)
8178

82-
const promise = factory('[no tx] ')
79+
const promise = factory(Promise, '[no tx] ')
8380

8481
plan.ok(!agent.tracer.getTransaction(), 'should not be in transaction')
8582
helper.runInTransaction(agent, function (tx) {
@@ -97,12 +94,11 @@ async function testPromiseContext({ t, factory }) {
9794
await t.test('context expiration', async function (t) {
9895
const plan = tspl(t, { plan: 2 })
9996
const { agent, Promise } = t.nr
100-
factory = factory.bind(null, Promise)
10197

10298
const ctxA = helper.runInTransaction(agent, function (tx) {
10399
return {
104100
transaction: tx,
105-
promise: factory('[tx a] ')
101+
promise: factory(Promise, '[tx a] ')
106102
}
107103
})
108104

test/versioned/bluebird/helpers.js

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
*/
55

66
'use strict'
7-
const assert = require('node:assert')
87
const { runMultiple } = require('../../lib/promises/helpers')
98
const { tspl } = require('@matteo.collina/tspl')
10-
const symbols = require('../../../lib/symbols')
119
const helper = require('../../lib/agent_helper')
1210
const { setImmediate } = require('timers/promises')
11+
const { removeModules } = require('../../lib/cache-buster')
1312

1413
async function beforeEach(ctx) {
1514
ctx.nr = {}
@@ -28,6 +27,7 @@ async function beforeEach(ctx) {
2827
async function afterEach(ctx) {
2928
helper.unloadAgent(ctx.nr.agent)
3029
clearInterval(ctx.nr.interval)
30+
removeModules(['bluebird'])
3131

3232
await setImmediate()
3333
}
@@ -123,27 +123,9 @@ function testPromiseMethod({ t, count, factory, end }) {
123123
}
124124
}
125125

126-
function areMethodsWrapped(source) {
127-
const methods = Object.keys(source).sort()
128-
methods.forEach((method) => {
129-
const wrapped = source[method]
130-
const original = wrapped[symbols.original]
131-
132-
// Skip this property if it is internal (starts or ends with underscore), is
133-
// a class (starts with a capital letter), or is not a function.
134-
if (/(?:^[_A-Z]|_$)/.test(method) || typeof original !== 'function') {
135-
return
136-
}
137-
138-
assert.ok(original, `${method} original exists`)
139-
assert.notEqual(wrapped, original, `${method} wrapped is not diff from original`)
140-
})
141-
}
142-
143126
module.exports = {
144127
addTask,
145128
afterEach,
146-
areMethodsWrapped,
147129
beforeEach,
148130
id,
149131
testPromiseClassMethod,

test/versioned/bluebird/promise-get-new-library-copy.test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@
66
'use strict'
77

88
// Some tests in this file need to assert that we handle non-error rejections:
9-
const assert = require('node:assert')
109
const test = require('node:test')
1110
const semver = require('semver')
12-
13-
const symbols = require('../../../lib/symbols')
14-
const helper = require('../../lib/agent_helper')
15-
11+
const { testPromiseContext } = require('./common-tests')
12+
const { beforeEach, afterEach } = require('./helpers')
1613
const { version: pkgVersion } = require('bluebird/package')
1714

18-
test('Promise.getNewLibraryCopy', { skip: semver.lt(pkgVersion, '3.4.1') }, function (t) {
19-
helper.loadTestAgent(t)
20-
const Promise = require('bluebird')
21-
const Promise2 = Promise.getNewLibraryCopy()
15+
test('Promise.getNewLibraryCopy', { skip: semver.lt(pkgVersion, '3.4.1') }, async function (t) {
16+
t.beforeEach((ctx) => {
17+
beforeEach(ctx)
18+
ctx.nr.Promise = ctx.nr.Promise.getNewLibraryCopy()
19+
})
20+
21+
t.afterEach(afterEach)
2222

23-
assert.ok(Promise2.resolve[symbols.original], 'should have wrapped class methods')
24-
assert.ok(Promise2.prototype.then[symbols.original], 'should have wrapped instance methods')
23+
await testPromiseContext({
24+
t,
25+
factory: function (Promise, name) {
26+
return Promise.resolve(name)
27+
}
28+
})
2529
})

0 commit comments

Comments
 (0)