diff --git a/src/error.rs b/src/error.rs index 70fb8b6..762edb8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,33 +6,17 @@ use std::fmt::{self, Display, Formatter}; #[derive(Debug)] pub enum OrchestrionError { - IoError(std::io::Error), - StrError(String), + InjectionMatchFailure(Vec), } -impl From for OrchestrionError { - fn from(e: std::io::Error) -> Self { - OrchestrionError::IoError(e) - } -} - -impl From for OrchestrionError { - fn from(s: String) -> Self { - OrchestrionError::StrError(s) - } -} - -impl From<&str> for OrchestrionError { - fn from(s: &str) -> Self { - OrchestrionError::StrError(s.to_string()) - } -} +impl std::error::Error for OrchestrionError {} impl Display for OrchestrionError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { - OrchestrionError::IoError(e) => write!(f, "IO error: {e}"), - OrchestrionError::StrError(s) => write!(f, "String error: {s}"), + OrchestrionError::InjectionMatchFailure(missing) => { + write!(f, "Failed to find injection points for: {missing:?}") + } } } } diff --git a/src/instrumentation.rs b/src/instrumentation.rs index e30c84f..7d988c6 100644 --- a/src/instrumentation.rs +++ b/src/instrumentation.rs @@ -29,9 +29,10 @@ macro_rules! ident { /// [`VisitMut`]: https://rustdoc.swc.rs/swc_core/ecma/visit/trait.VisitMut.html #[derive(Debug, Clone)] pub struct Instrumentation { - config: InstrumentationConfig, + pub(crate) config: InstrumentationConfig, count: usize, is_correct_class: bool, + has_injected: bool, } impl Instrumentation { @@ -41,14 +42,24 @@ impl Instrumentation { config, count: 0, is_correct_class: false, + has_injected: false, } } + #[must_use] + pub fn has_injected(&self) -> bool { + self.has_injected + } + pub(crate) fn reset(&mut self) { self.count = 0; self.is_correct_class = false; } + pub(crate) fn reset_has_injected(&mut self) { + self.has_injected = false; + } + fn new_fn(&self, body: BlockStmt) -> ArrowExpr { ArrowExpr { params: vec![], @@ -114,6 +125,8 @@ impl Instrumentation { trace = trace_ident ), ]; + + self.has_injected = true; } fn insert_constructor_tracing(&mut self, body: &mut BlockStmt) { @@ -160,6 +173,8 @@ impl Instrumentation { quote!("const $ctx = { arguments };" as Stmt, ctx = ctx_ident,), try_catch, ]; + + self.has_injected = true; } fn trace_expr_or_count(&mut self, func_expr: &mut FnExpr, name: &Atom) -> bool { diff --git a/src/lib.rs b/src/lib.rs index db13abd..5d36e43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,8 @@ pub use instrumentation::*; mod function_query; pub use function_query::*; +use crate::error::OrchestrionError; + #[cfg(feature = "wasm")] pub mod wasm; @@ -112,6 +114,33 @@ impl InstrumentationVisitor { !self.instrumentations.is_empty() } + #[must_use] + pub fn get_failed_injections(&self) -> Option> { + let failed: Vec = self + .instrumentations + .iter() + .filter_map(|instr| { + if instr.has_injected() { + None + } else { + Some(instr.config.function_query.name().to_string()) + } + }) + .collect(); + + if failed.is_empty() { + None + } else { + Some(failed) + } + } + + pub fn reset_has_injected(&mut self) { + for instr in &mut self.instrumentations { + instr.reset_has_injected(); + } + } + /// Transform the given JavaScript code. /// # Errors /// Returns an error if the transformation fails. @@ -125,7 +154,7 @@ impl InstrumentationVisitor { ))); #[allow(clippy::redundant_closure_for_method_calls)] - Ok(try_with_handler( + let result = try_with_handler( compiler.cm.clone(), HandlerOpts { color: ColorConfig::Never, @@ -165,10 +194,20 @@ impl InstrumentationVisitor { ..Default::default() }, )?; + Ok(result.code) }, ) - .map_err(|e| e.to_pretty_error())?) + .map_err(|e| e.to_pretty_error())?; + + let failed_injections = self.get_failed_injections(); + self.reset_has_injected(); + + if let Some(failed) = failed_injections { + Err(Box::new(OrchestrionError::InjectionMatchFailure(failed))) + } else { + Ok(result) + } } } diff --git a/src/wasm.rs b/src/wasm.rs index 9fcb5be..43ebdd4 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,5 +1,7 @@ -use crate::*; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use crate::{Config, InstrumentationConfig, InstrumentationVisitor, Instrumentor}; +use std::path::PathBuf; +use swc::config::IsModule; +use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct InstrumentationMatcher(Instrumentor); @@ -31,11 +33,11 @@ pub struct Transformer(InstrumentationVisitor); #[wasm_bindgen] impl Transformer { #[wasm_bindgen] - pub fn transform(&mut self, contents: &str, is_module: bool) -> Result { + pub fn transform(&mut self, contents: &str, is_module: bool) -> Result { let is_module = IsModule::Bool(is_module); self.0 .transform(contents, is_module) - .map_err(|e| JsValue::from_str(&e.to_string())) + .map_err(|e| JsError::new(&e.to_string())) } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index b86ed47..650fe86 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -27,13 +27,11 @@ pub fn transpile_and_test(test_file: &str, mjs: bool, config: Config) { let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); - let result = instrumentations - .transform(&contents, IsModule::Bool(mjs)) - .unwrap(); - - let instrumented_file = test_dir.join(format!("instrumented.{}", extension)); - let mut file = std::fs::File::create(&instrumented_file).unwrap(); - file.write_all(result.as_bytes()).unwrap(); + if let Ok(result) = instrumentations.transform(&contents, IsModule::Bool(mjs)) { + let instrumented_file = test_dir.join(format!("instrumented.{}", extension)); + let mut file = std::fs::File::create(&instrumented_file).unwrap(); + file.write_all(result.as_bytes()).unwrap(); + } let test_file = format!("test.{}", extension); Command::new("node") diff --git a/tests/injection_failure/mod.mjs b/tests/injection_failure/mod.mjs new file mode 100644 index 0000000..9cf4646 --- /dev/null +++ b/tests/injection_failure/mod.mjs @@ -0,0 +1,9 @@ +/** + * 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 = async function (url) { + return 42; +} + +export { fetch }; diff --git a/tests/injection_failure/mod.rs b/tests/injection_failure/mod.rs new file mode 100644 index 0000000..f824072 --- /dev/null +++ b/tests/injection_failure/mod.rs @@ -0,0 +1,15 @@ +use crate::common::*; +use orchestrion_js::*; + +#[test] +fn injection_failure() { + transpile_and_test( + file!(), + true, + Config::new_single(InstrumentationConfig::new( + "some_expr", + test_module_matcher(), + FunctionQuery::function_expression("some", FunctionKind::Async), + )), + ); +} diff --git a/tests/injection_failure/test.mjs b/tests/injection_failure/test.mjs new file mode 100644 index 0000000..5b860ae --- /dev/null +++ b/tests/injection_failure/test.mjs @@ -0,0 +1,9 @@ +/** + * 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. + **/ +import { existsSync } from 'node:fs' +import { assert } from '../common/preamble.js'; + +const instrumented = existsSync('./instrumented.js'); +assert.strictEqual(instrumented, false, 'instrumented.js should not exist'); diff --git a/tests/instrumentor_test.rs b/tests/instrumentor_test.rs index f510e3b..30e103b 100644 --- a/tests/instrumentor_test.rs +++ b/tests/instrumentor_test.rs @@ -14,6 +14,7 @@ mod decl_mjs_mismatched_type; mod expr_cjs; mod expr_mjs; mod index_cjs; +mod injection_failure; mod multiple_class_method_cjs; mod multiple_load_cjs; mod object_method_cjs; diff --git a/tests/wasm/testdata/no-match.mjs b/tests/wasm/testdata/no-match.mjs new file mode 100644 index 0000000..713cf22 --- /dev/null +++ b/tests/wasm/testdata/no-match.mjs @@ -0,0 +1,9 @@ +export class Down { + constructor() { + console.log('constructor') + } + + fetch() { + console.log('fetch') + } +} \ No newline at end of file diff --git a/tests/wasm/tests.mjs b/tests/wasm/tests.mjs index bbc3e2d..773d522 100644 --- a/tests/wasm/tests.mjs +++ b/tests/wasm/tests.mjs @@ -40,3 +40,9 @@ const outputCjs = matchedTransforms.transform(originalCjs.toString('utf8'), fals const expectedCjs = await fs.readFile(path.join(import.meta.dirname, './testdata/expected-cjs.js')) assert.strictEqual(outputCjs, expectedCjs.toString('utf8')); + +const noMatch = await fs.readFile(path.join(import.meta.dirname, './testdata/no-match.mjs')); + +assert.throws(() => { + matchedTransforms.transform(noMatch.toString('utf8'), true); +}, { message: "Failed to find injection points for: [\"constructor\", \"fetch\"]" });