Skip to content
Closed
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -25,4 +25,4 @@
"volta": {
"node": "22.15.0"
}
}
}
34 changes: 24 additions & 10 deletions src/instrumentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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<Pat>) -> ArrowExpr {
ArrowExpr {
params: vec![],
params,
body: Box::new(body.into()),
is_async: self.config.function_query.kind().is_async(),
is_generator: false,
Expand Down Expand Up @@ -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);
Expand All @@ -104,7 +104,20 @@ impl Instrumentation {
..body.clone()
};

let traced_fn = self.new_fn(original_body);
let original_params: Vec<Pat> = 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));
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions tests/arguments_mutation/mod.js
Original file line number Diff line number Diff line change
@@ -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 };
25 changes: 25 additions & 0 deletions tests/arguments_mutation/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
),
);
}
38 changes: 38 additions & 0 deletions tests/arguments_mutation/test.js
Original file line number Diff line number Diff line change
@@ -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');
2 changes: 2 additions & 0 deletions tests/instrumentor_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
**/
mod common;

mod arguments_mutation;
mod class_expression_cjs;
mod class_method_cjs;
mod constructor_cjs;
Expand All @@ -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;
18 changes: 18 additions & 0 deletions tests/nested_functions/mod.js
Original file line number Diff line number Diff line change
@@ -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;
15 changes: 15 additions & 0 deletions tests/nested_functions/mod.rs
Original file line number Diff line number Diff line change
@@ -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),
)),
);
}
16 changes: 16 additions & 0 deletions tests/nested_functions/test.js
Original file line number Diff line number Diff line change
@@ -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,
});
})();
6 changes: 5 additions & 1 deletion tests/wasm/testdata/expected-cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
6 changes: 5 additions & 1 deletion tests/wasm/testdata/expected.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
1 change: 0 additions & 1 deletion tests/wasm/tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down