diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ac5c1..2dd5d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.6.0 + +- fix: Allow for argumentation mutation in complex argument functions (#19) +## 0.5.0 + +- fix: Allow injecting into functions nested in functions (#17) + ## 0.4.0 - feat: Error when code injection fails (#9) diff --git a/package.json b/package.json index c3096fe..05286e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apm-js-collab/code-transformer", - "version": "0.4.0", + "version": "0.6.0", "license": "Apache-2.0", "repository": { "type": "git", @@ -25,4 +25,4 @@ "volta": { "node": "22.15.0" } -} \ No newline at end of file +} diff --git a/src/instrumentation.rs b/src/instrumentation.rs index 9e6a2e7..0e7c580 100644 --- a/src/instrumentation.rs +++ b/src/instrumentation.rs @@ -9,7 +9,7 @@ use swc_core::ecma::{ ast::{ ArrowExpr, AssignExpr, AssignTarget, BlockStmt, ClassDecl, ClassExpr, ClassMethod, Constructor, Expr, FnDecl, FnExpr, Ident, Lit, MemberProp, MethodProp, Module, ModuleItem, - Pat, PropName, Script, SimpleAssignTarget, Stmt, Str, VarDecl, + Param, Pat, PropName, Script, SimpleAssignTarget, Stmt, Str, VarDecl, }, atoms::Atom, }; @@ -60,9 +60,9 @@ impl Instrumentation { self.has_injected = false; } - fn new_fn(&self, body: BlockStmt) -> ArrowExpr { + fn new_fn(&self, body: BlockStmt, params: Vec) -> ArrowExpr { ArrowExpr { - params: vec![], + params, body: Box::new(body.into()), is_async: self.config.function_query.kind().is_async(), is_generator: false, @@ -92,7 +92,7 @@ impl Instrumentation { define_channel } - fn insert_tracing(&mut self, body: &mut BlockStmt) { + fn insert_tracing(&mut self, body: &mut BlockStmt, params: &[Param]) { self.count += 1; let original_stmts = std::mem::take(&mut body.stmts); @@ -104,7 +104,20 @@ impl Instrumentation { ..body.clone() }; - let traced_fn = self.new_fn(original_body); + let original_params: Vec = params.iter().map(|p| p.pat.clone()).collect(); + + let wrapped_fn = self.new_fn(original_body, original_params); + + let traced_body = BlockStmt { + span: Span::default(), + ctxt: SyntaxContext::empty(), + stmts: vec![ + quote!("const __apm$wrapped = $wrapped;" as Stmt, wrapped: Expr = wrapped_fn.into()), + quote!("return __apm$wrapped.apply(null, __apm$original_args);" as Stmt), + ], + }; + + let traced_fn = self.new_fn(traced_body, vec![]); let id_name = self.config.get_identifier_name(); let ch_ident = ident!(format!("tr_ch_apm${}", &id_name)); @@ -115,6 +128,7 @@ impl Instrumentation { )); body.stmts = vec![ + quote!("const __apm$original_args = arguments" as Stmt), quote!("const __apm$traced = $traced;" as Stmt, traced: Expr = traced_fn.into()), quote!( "if (!$ch.hasSubscribers) return __apm$traced();" as Stmt, @@ -185,7 +199,7 @@ impl Instrumentation { && func_expr.function.body.is_some() { if let Some(body) = func_expr.function.body.as_mut() { - self.insert_tracing(body); + self.insert_tracing(body, &func_expr.function.params); } true } else { @@ -223,10 +237,10 @@ impl Instrumentation { && node.function.body.is_some() { if let Some(body) = node.function.body.as_mut() { - self.insert_tracing(body); + self.insert_tracing(body, &node.function.params); } } - false + true } pub fn visit_mut_var_decl(&mut self, node: &mut VarDecl) -> bool { @@ -279,7 +293,7 @@ impl Instrumentation { && node.function.body.is_some() { if let Some(body) = node.function.body.as_mut() { - self.insert_tracing(body); + self.insert_tracing(body, &node.function.params); } } true @@ -312,7 +326,7 @@ impl Instrumentation { && node.function.body.is_some() { if let Some(body) = node.function.body.as_mut() { - self.insert_tracing(body); + self.insert_tracing(body, &node.function.params); } } false diff --git a/tests/arguments_mutation/mod.js b/tests/arguments_mutation/mod.js new file mode 100644 index 0000000..f648da1 --- /dev/null +++ b/tests/arguments_mutation/mod.js @@ -0,0 +1,30 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2025 Datadog, Inc. + **/ +const assert = require('assert'); + +function fetch_simple (url, cb) { + assert.strictEqual(this.this, 'this'); + assert.strictEqual(url, 'https://example.com'); + assert.strictEqual(cb.length, 2); + const result = cb.apply(this, ['arg1', 'arg2']); + assert.strictEqual(result, 'result'); + return 'return'; +} + +function fetch_complex ({ url, tuple: [a = 'a', b = 'b'] }, cb, optional = 'default', ...rest) { + assert.strictEqual(this.this, 'this'); + assert.strictEqual(url, 'https://example.com'); + assert.strictEqual(a, 'a'); + assert.strictEqual(b, 'b'); + assert.strictEqual(cb.length, 2); + assert.strictEqual(optional, 'default'); + assert.deepStrictEqual(rest, []); + const result = cb.apply(this, ['arg1', 'arg2']); + assert.strictEqual(result, 'result'); + return 'return'; +} + + +module.exports = { fetch_simple, fetch_complex }; diff --git a/tests/arguments_mutation/mod.rs b/tests/arguments_mutation/mod.rs new file mode 100644 index 0000000..584a249 --- /dev/null +++ b/tests/arguments_mutation/mod.rs @@ -0,0 +1,25 @@ +use crate::common::*; +use orchestrion_js::*; + +#[test] +fn arguments_mutation() { + transpile_and_test( + file!(), + false, + Config::new( + vec![ + InstrumentationConfig::new( + "fetch_simple", + test_module_matcher(), + FunctionQuery::function_declaration("fetch_simple", FunctionKind::Sync), + ), + InstrumentationConfig::new( + "fetch_complex", + test_module_matcher(), + FunctionQuery::function_declaration("fetch_complex", FunctionKind::Sync), + ), + ], + None, + ), + ); +} diff --git a/tests/arguments_mutation/test.js b/tests/arguments_mutation/test.js new file mode 100644 index 0000000..42de75b --- /dev/null +++ b/tests/arguments_mutation/test.js @@ -0,0 +1,38 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2025 Datadog, Inc. + **/ +const { fetch_simple, fetch_complex } = require('./instrumented.js'); +const assert = require('assert'); +const { tracingChannel } = require('diagnostics_channel'); + +const handler = { + start (message) { + const originalCb = message.arguments[1]; + const wrappedCb = function (a, b) { + assert.strictEqual(this.this, 'this'); + assert.strictEqual(a, 'arg1'); + assert.strictEqual(b, 'arg2'); + arguments[1] = 'arg2_mutated'; + return originalCb.apply(this, arguments); + } + + message.arguments[1] = wrappedCb; + } +}; + +tracingChannel('orchestrion:undici:fetch_simple').subscribe(handler); +tracingChannel('orchestrion:undici:fetch_complex').subscribe(handler); + +assert.strictEqual(fetch_simple.length, 2); +assert.strictEqual(fetch_complex.length, 2); + +const cb = function (a, b) { + assert.strictEqual(this.this, 'this'); + assert.strictEqual(a, 'arg1'); + assert.strictEqual(b, 'arg2_mutated'); + return 'result'; +}; + +assert.strictEqual(fetch_simple.apply({ this: 'this' }, ['https://example.com', cb]), 'return'); +assert.strictEqual(fetch_complex.apply({ this: 'this' }, [{ url: 'https://example.com', tuple: [] }, cb]), 'return'); diff --git a/tests/instrumentor_test.rs b/tests/instrumentor_test.rs index 30e103b..6e2f7e9 100644 --- a/tests/instrumentor_test.rs +++ b/tests/instrumentor_test.rs @@ -4,6 +4,7 @@ **/ mod common; +mod arguments_mutation; mod class_expression_cjs; mod class_method_cjs; mod constructor_cjs; @@ -17,6 +18,7 @@ mod index_cjs; mod injection_failure; mod multiple_class_method_cjs; mod multiple_load_cjs; +mod nested_functions; mod object_method_cjs; mod polyfill_cjs; mod polyfill_mjs; diff --git a/tests/nested_functions/mod.js b/tests/nested_functions/mod.js new file mode 100644 index 0000000..979b60d --- /dev/null +++ b/tests/nested_functions/mod.js @@ -0,0 +1,18 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2025 Datadog, Inc. + **/ + +function fastify() { + const fastify = { + addHook + } + + function addHook() { + return 'Hook added'; + } + + return fastify +} + +module.exports = fastify; diff --git a/tests/nested_functions/mod.rs b/tests/nested_functions/mod.rs new file mode 100644 index 0000000..820cd81 --- /dev/null +++ b/tests/nested_functions/mod.rs @@ -0,0 +1,15 @@ +use crate::common::*; +use orchestrion_js::*; + +#[test] +fn nested_fn() { + transpile_and_test( + file!(), + false, + Config::new_single(InstrumentationConfig::new( + "nested_fn", + test_module_matcher(), + FunctionQuery::function_declaration("addHook", FunctionKind::Sync), + )), + ); +} diff --git a/tests/nested_functions/test.js b/tests/nested_functions/test.js new file mode 100644 index 0000000..e1091b8 --- /dev/null +++ b/tests/nested_functions/test.js @@ -0,0 +1,16 @@ +/** + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache-2.0 License. + * This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2025 Datadog, Inc. + **/ +const fastify = require('./instrumented.js'); +const { assert, getContext } = require('../common/preamble.js'); +const context = getContext('orchestrion:undici:nested_fn'); +(async () => { + const f = fastify() + const result = f.addHook() + assert.strictEqual(result, 'Hook added'); + assert.deepStrictEqual(context, { + start: true, + end: true, + }); +})(); diff --git a/tests/wasm/testdata/expected-cjs.js b/tests/wasm/testdata/expected-cjs.js index ac73dd5..8d5ec98 100644 --- a/tests/wasm/testdata/expected-cjs.js +++ b/tests/wasm/testdata/expected-cjs.js @@ -28,8 +28,12 @@ module.exports = class Up { } } fetch() { + const __apm$original_args = arguments; const __apm$traced = ()=>{ - console.log('fetch'); + const __apm$wrapped = ()=>{ + console.log('fetch'); + }; + return __apm$wrapped.apply(null, __apm$original_args); }; if (!tr_ch_apm$up_fetch.hasSubscribers) return __apm$traced(); return tr_ch_apm$up_fetch.traceSync(__apm$traced, { diff --git a/tests/wasm/testdata/expected.mjs b/tests/wasm/testdata/expected.mjs index 5287555..67b4ff4 100644 --- a/tests/wasm/testdata/expected.mjs +++ b/tests/wasm/testdata/expected.mjs @@ -28,8 +28,12 @@ export class Up { } } fetch() { + const __apm$original_args = arguments; const __apm$traced = ()=>{ - console.log('fetch'); + const __apm$wrapped = ()=>{ + console.log('fetch'); + }; + return __apm$wrapped.apply(null, __apm$original_args); }; if (!tr_ch_apm$up_fetch.hasSubscribers) return __apm$traced(); return tr_ch_apm$up_fetch.traceSync(__apm$traced, { diff --git a/tests/wasm/tests.mjs b/tests/wasm/tests.mjs index 53ff8cd..b90d601 100644 --- a/tests/wasm/tests.mjs +++ b/tests/wasm/tests.mjs @@ -37,7 +37,6 @@ assert.strictEqual(output, expected.toString('utf8')); const originalCjs = await fs.readFile(path.join(import.meta.dirname, './testdata/original-cjs.js')) const outputCjs = matchedTransforms.transform(originalCjs.toString('utf8'), 'cjs'); - const expectedCjs = await fs.readFile(path.join(import.meta.dirname, './testdata/expected-cjs.js')) assert.strictEqual(outputCjs, expectedCjs.toString('utf8'));