From 0d6f9a697a6b01217fe90496dad1e60d37755afb Mon Sep 17 00:00:00 2001 From: Bryan English Date: Tue, 23 Jun 2026 16:33:42 -0400 Subject: [PATCH 01/12] build: set up cargo workspace and wasm/native build tooling Establish the Rust workspace, pin the toolchain, and add the npm scripts and shell tooling that build the wasm modules and the native (napi) addons and run their test suites. Co-authored-by: Jules Wiriath Co-authored-by: paullegranddc Co-authored-by: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> --- Cargo.lock | 734 ++++++++++++++++++++++++++++++++++++-------- Cargo.toml | 6 +- package.json | 5 +- rust-toolchain.toml | 2 +- scripts/test.sh | 15 +- yarn.lock | 695 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 1321 insertions(+), 136 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afac19e..2c8d4f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -63,9 +72,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -73,9 +82,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -91,9 +100,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blazesym" @@ -101,7 +110,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48ceccc54b9c3e60e5f36b0498908c8c0f87387229cb0e0e5d65a074e00a8ba4" dependencies = [ - "cpp_demangle", + "cpp_demangle 0.5.1", "gimli", "libc", "memmap2", @@ -109,6 +118,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -118,6 +136,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bumpalo" version = "3.20.2" @@ -130,6 +154,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cadence" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca08f6db9f0c963249cf23a27820f9d973d2acaad4d6bfaeb858b58ebd58a6a" +dependencies = [ + "crossbeam-channel", +] + [[package]] name = "cast" version = "0.3.0" @@ -138,9 +171,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -174,20 +207,31 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -226,6 +270,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpp_demangle" version = "0.5.1" @@ -235,6 +288,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crashtracker" version = "0.2.0" @@ -247,6 +309,31 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "ctor" version = "0.2.9" @@ -267,20 +354,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "datadog-library-config" -version = "0.0.2" -source = "git+https://github.com/DataDog/libdatadog.git?tag=v18.1.0#5fdc3357f0070d5ad11dcaf89014b7479d32104f" -dependencies = [ - "anyhow", - "memfd", - "rand", - "rmp", - "rmp-serde", - "serde", - "serde_yaml", -] - [[package]] name = "debugid" version = "0.8.0" @@ -290,6 +363,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -346,6 +429,17 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fluent-uri" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "foldhash" version = "0.1.5" @@ -446,6 +540,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -508,9 +612,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -565,9 +669,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -578,7 +682,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -586,16 +689,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -653,12 +755,12 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -674,9 +776,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -690,14 +792,31 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "leb128fmt" version = "0.1.0" @@ -706,9 +825,24 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdatadog-nodejs-capabilities" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "futures-core", + "http", + "js-sys", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] [[package]] name = "libdd-capabilities" @@ -721,6 +855,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "libdd-capabilities" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "bytes", + "http", + "thiserror", +] + [[package]] name = "libdd-capabilities-impl" version = "2.0.0" @@ -729,8 +874,21 @@ dependencies = [ "bytes", "http", "http-body-util", - "libdd-capabilities", - "libdd-common", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-common 4.2.0", + "tokio", +] + +[[package]] +name = "libdd-capabilities-impl" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "bytes", + "http", + "http-body-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", "tokio", ] @@ -768,6 +926,36 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "libdd-common" +version = "5.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "bytes", + "cc", + "const_format", + "futures", + "futures-core", + "futures-util", + "hex", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "libc", + "nix 0.29.0", + "pin-project", + "regex", + "serde", + "static_assertions", + "thiserror", + "tokio", + "tower-service", + "windows-sys 0.52.0", +] + [[package]] name = "libdd-crashtracker" version = "1.0.0" @@ -780,7 +968,7 @@ dependencies = [ "errno", "http", "libc", - "libdd-common", + "libdd-common 4.2.0", "libdd-libunwind-sys", "libdd-telemetry", "nix 0.29.0", @@ -801,6 +989,40 @@ dependencies = [ "windows 0.59.0", ] +[[package]] +name = "libdd-data-pipeline" +version = "6.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "arc-swap", + "async-trait", + "bytes", + "either", + "getrandom 0.2.17", + "http", + "http-body-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-dogstatsd-client", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-tinybytes", + "libdd-trace-normalization", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-stats", + "libdd-trace-utils", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + [[package]] name = "libdd-ddsketch" version = "1.0.1" @@ -809,13 +1031,34 @@ dependencies = [ "prost", ] +[[package]] +name = "libdd-ddsketch" +version = "1.0.1" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "prost", +] + +[[package]] +name = "libdd-dogstatsd-client" +version = "3.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "cadence", + "http", + "libdd-common 5.0.0", + "serde", + "tracing", +] + [[package]] name = "libdd-library-config" version = "1.0.0" source = "git+https://github.com/DataDog/libdatadog.git?tag=v29.0.0#001bd56fcbba34fa4ec3f9798a6c4fbcddeffa40" dependencies = [ "anyhow", - "libdd-trace-protobuf", + "libdd-trace-protobuf 1.1.0", "memfd", "prost", "rand", @@ -826,6 +1069,20 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "libdd-library-config" +version = "1.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?rev=353134770b312b7ccd2df6afabc253090b948e5f#353134770b312b7ccd2df6afabc253090b948e5f" +dependencies = [ + "anyhow", + "memfd", + "rand", + "rmp", + "rmp-serde", + "serde", + "serde_yaml", +] + [[package]] name = "libdd-libunwind-sys" version = "1.0.2" @@ -845,9 +1102,26 @@ dependencies = [ "async-trait", "futures", "futures-util", - "libdd-capabilities", - "libdd-capabilities-impl", - "libdd-common", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-common 4.2.0", + "tokio", + "tokio-util", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "libdd-shared-runtime" +version = "1.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "async-trait", + "futures", + "futures-util", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", "tokio", "tokio-util", "tracing", @@ -868,9 +1142,9 @@ dependencies = [ "http", "http-body-util", "libc", - "libdd-common", - "libdd-ddsketch", - "libdd-shared-runtime", + "libdd-common 4.2.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v35.0.0)", "serde", "serde_json", "sys-info", @@ -881,6 +1155,39 @@ dependencies = [ "winver", ] +[[package]] +name = "libdd-tinybytes" +version = "1.1.1" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "serde", +] + +[[package]] +name = "libdd-trace-normalization" +version = "2.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "libdd-trace-protobuf 3.0.2", +] + +[[package]] +name = "libdd-trace-obfuscation" +version = "4.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "fluent-uri", + "libdd-common 5.0.0", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-utils", + "log", + "percent-encoding", + "serde", + "serde_json", +] + [[package]] name = "libdd-trace-protobuf" version = "1.1.0" @@ -891,6 +1198,75 @@ dependencies = [ "serde_bytes", ] +[[package]] +name = "libdd-trace-protobuf" +version = "3.0.2" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "prost", + "serde", + "serde_bytes", +] + +[[package]] +name = "libdd-trace-stats" +version = "5.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "arc-swap", + "async-trait", + "hashbrown 0.15.5", + "http", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-ddsketch 1.0.1 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-trace-obfuscation", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-utils", + "rmp-serde", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "libdd-trace-utils" +version = "8.0.0" +source = "git+https://github.com/DataDog/libdatadog.git?branch=main#4b79b7ed87113bea01db583d54e13fb0c2a19e74" +dependencies = [ + "anyhow", + "base64", + "bytes", + "futures", + "getrandom 0.2.17", + "hex", + "http", + "http-body", + "http-body-util", + "indexmap", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-capabilities-impl 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-common 5.0.0", + "libdd-tinybytes", + "libdd-trace-normalization", + "libdd-trace-protobuf 3.0.2", + "prost", + "rand", + "rmp", + "rmp-serde", + "rmpv", + "rustc-hash", + "serde", + "serde_json", + "thin-vec", + "tokio", + "tracing", +] + [[package]] name = "libloading" version = "0.8.9" @@ -912,8 +1288,8 @@ name = "library-config" version = "0.2.0" dependencies = [ "anyhow", - "datadog-library-config", "getrandom 0.2.17", + "libdd-library-config 1.0.0 (git+https://github.com/DataDog/libdatadog.git?rev=353134770b312b7ccd2df6afabc253090b948e5f)", "serde", "serde-wasm-bindgen", "wasm-bindgen", @@ -977,9 +1353,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5faa9f23e86bd5768d76def086192ff5f869fb088da12a976ea21e9796b975f6" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -987,9 +1363,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -1273,9 +1649,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -1321,6 +1697,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project" version = "1.1.11" @@ -1348,16 +1730,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" +name = "pipeline" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +dependencies = [ + "bytes", + "console_error_panic_hook", + "getrandom 0.2.17", + "http", + "js-sys", + "libdatadog-nodejs-capabilities", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-data-pipeline", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-trace-protobuf 3.0.2", + "libdd-trace-stats", + "libdd-trace-utils", + "rmp-serde", + "serde", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1401,7 +1801,7 @@ name = "process-discovery" version = "0.1.0" dependencies = [ "anyhow", - "libdd-library-config", + "libdd-library-config 1.0.0 (git+https://github.com/DataDog/libdatadog.git?tag=v29.0.0)", "napi", "napi-derive", ] @@ -1452,9 +1852,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", @@ -1480,6 +1880,26 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" version = "1.12.3" @@ -1542,12 +1962,27 @@ dependencies = [ "serde", ] +[[package]] +name = "rmpv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" +dependencies = [ + "rmp", +] + [[package]] name = "rustc-demangle" version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustix" version = "1.1.4" @@ -1563,9 +1998,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", @@ -1590,18 +2025,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -1688,9 +2123,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1760,6 +2195,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -1780,6 +2216,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1788,9 +2235,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" @@ -1834,9 +2281,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751a2823d606b5d0a7616499e4130a516ebd01a44f39811be2b9600936509c23" +checksum = "332615d90111d8eeaf86a84dc9bbe9f65d0d8c5cf11b4caccedc37754eb0dcfd" dependencies = [ "debugid", "memmap2", @@ -1846,11 +2293,11 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.17.2" +version = "12.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b237cfbe320601dd24b4ac817a5b68bb28f5508e33f08d42be0682cadc8ac9" +checksum = "912017718eb4d21930546245af9a3475c9dccf15675a5c215664e76621afc471" dependencies = [ - "cpp_demangle", + "cpp_demangle 0.4.5", "msvc-demangler", "rustc-demangle", "symbolic-common", @@ -1877,6 +2324,12 @@ dependencies = [ "libc", ] +[[package]] +name = "thin-vec" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7e269b48f0a7dd0146680fa24b50cc67fc0373f086a5b2f99bd084639b482" + [[package]] name = "thiserror" version = "1.0.69" @@ -1899,9 +2352,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -1914,9 +2367,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -1952,6 +2405,23 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "trace-exporter" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "getrandom 0.2.17", + "js-sys", + "libdatadog-nodejs-capabilities", + "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "libdd-data-pipeline", + "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", +] + [[package]] name = "tracing" version = "0.1.44" @@ -1977,6 +2447,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1985,9 +2461,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2009,9 +2485,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -2019,6 +2495,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -2046,11 +2528,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2059,14 +2541,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -2077,23 +2559,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2101,9 +2579,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -2114,18 +2592,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.64" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +checksum = "29826f9d9ecaa314c480d376b276d1c790e6cb6a4681fab8532da69cbabf977d" dependencies = [ "async-trait", "cast", @@ -2145,9 +2623,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.64" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +checksum = "c610311887f9e6599a546d278d12d69dfd3a3e92639b2129e4b11ad6cf1961d6" dependencies = [ "proc-macro2", "quote", @@ -2156,9 +2634,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-shared" -version = "0.2.114" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" +checksum = "60238e5b4b1b295701d6f9a66d2a126fe19990348f5fb9dae3b623a370119d94" [[package]] name = "wasm-encoder" @@ -2194,16 +2672,6 @@ dependencies = [ "semver", ] -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2583,6 +3051,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -2664,18 +3138,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4508363..24099b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" default-members = [ "crates/crashtracker", "crates/process_discovery", @@ -12,6 +13,5 @@ codegen-units = 1 lto = true opt-level = "z" panic = "abort" -strip = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# strip = "none" +debug = true diff --git a/package.json b/package.json index 6af6637..df07da2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build-debug": "mkdir -p target && yarn -s cargo-build > ./target/out.ndjson && yarn -s copy-artifacts", "build-release": "mkdir -p target && yarn -s cargo-build-release > ./target/out.ndjson && yarn -s copy-artifacts", "build-all": "mkdir -p target && yarn -s cargo-build -- --workspace > ./target/out.ndjson && yarn -s copy-artifacts && yarn -s build-wasm", - "build-wasm": "yarn -s install-wasm-pack && node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd", + "build-wasm": "node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js trace_exporter && node scripts/build-wasm.js pipeline", "cargo-build-release": "yarn -s cargo-build -- --release", "cargo-build": "cargo build --message-format=json-render-diagnostics", "copy-artifacts": "node ./scripts/copy-artifacts", @@ -29,6 +29,9 @@ "publishConfig": { "access": "public" }, + "dependencies": { + "@napi-rs/cli": "^3.6.0" + }, "devDependencies": { "@eslint/js": "^10.0.1", "@stylistic/eslint-plugin": "^5.9.0", diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 9b112d4..4001ea7 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.87.0" +channel = "1.90.0" profile = "minimal" components = ["clippy", "rustfmt", "rust-src"] diff --git a/scripts/test.sh b/scripts/test.sh index cfd0ca4..7a33a8b 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -9,7 +9,16 @@ run_test() { yarn --cwd "$dir" install fi echo "Running $1" - node "$1" + # node:test does not force the process to exit when the event loop is kept + # active by async work that has already settled (e.g. the wasm trace + # exporter's runtime machinery after a flush). For the long-lived real + # consumer that is expected; for the test runner we force a clean exit once + # all tests have finished. Only applies to files that use node:test. + if grep -q "node:test" "$1"; then + node --test-force-exit "$1" + else + node "$1" + fi } # Run top-level test files @@ -26,3 +35,7 @@ for d in test/*/; do ;; esac done + +# The wasm trace exporter integration test runs against its own in-process mock +# agent, so run it directly (other test/wasm/* modules remain manual for now). +run_test test/wasm/trace_exporter/index.js diff --git a/yarn.lock b/yarn.lock index c86291b..5c4d745 100644 --- a/yarn.lock +++ b/yarn.lock @@ -105,6 +105,386 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@inquirer/ansi@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-2.0.7.tgz#86de22810cac3ed406ec10f8d66016815b8226b4" + integrity sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q== + +"@inquirer/checkbox@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-5.2.1.tgz#7f148b3153a776cee202015b10f9a985068d188d" + integrity sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/confirm@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-6.1.1.tgz#9c6a7d79c6132b2af57fdb75747f056204e55356" + integrity sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/core@^11.2.1": + version "11.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-11.2.1.tgz#54ccd8f7d47852140b6066cbd77d63b2c2b168fd" + integrity sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + cli-width "^4.1.0" + fast-wrap-ansi "^0.2.0" + mute-stream "^3.0.0" + signal-exit "^4.1.0" + +"@inquirer/editor@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-5.2.2.tgz#7c73e2fc0e7bd4c40cfd38a180ae5bbd24d32b90" + integrity sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/external-editor" "^3.0.3" + "@inquirer/type" "^4.0.7" + +"@inquirer/expand@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-5.1.1.tgz#e2afeac247d97dd64ee18aa81e902bdd1fe0ea70" + integrity sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/external-editor@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-3.0.3.tgz#d79e772542cf8d340642e9dabd3a1ea7f5a30104" + integrity sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA== + dependencies: + chardet "^2.1.1" + iconv-lite "^0.7.2" + +"@inquirer/figures@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-2.0.7.tgz#f5cc5843732a81304d06a0db4b53cc7dbda15541" + integrity sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw== + +"@inquirer/input@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-5.1.2.tgz#9305cb170dfc3a5323e5eac885a945e7cddd5c4b" + integrity sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/number@^4.1.1": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-4.1.1.tgz#b133668d8e0e099b4133abb915221501e0ff75d7" + integrity sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/password@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-5.1.1.tgz#f21efb614da9c905095262f51781fd2a721fceac" + integrity sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/prompts@^8.5.2": + version "8.5.2" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-8.5.2.tgz#09c0132ada2bbba94c91d341115e1e41cb3f1525" + integrity sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g== + dependencies: + "@inquirer/checkbox" "^5.2.1" + "@inquirer/confirm" "^6.1.1" + "@inquirer/editor" "^5.2.2" + "@inquirer/expand" "^5.1.1" + "@inquirer/input" "^5.1.2" + "@inquirer/number" "^4.1.1" + "@inquirer/password" "^5.1.1" + "@inquirer/rawlist" "^5.3.1" + "@inquirer/search" "^4.2.1" + "@inquirer/select" "^5.2.1" + +"@inquirer/rawlist@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-5.3.1.tgz#66f6b8e6aa82d47399c433b8262128e7c1a4f9ce" + integrity sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/type" "^4.0.7" + +"@inquirer/search@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-4.2.1.tgz#c8f4b78ab3f866fdf0503fac0cd08c4a6661c11e" + integrity sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g== + dependencies: + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/select@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-5.2.1.tgz#3a05e76e58d9e1bb095e912c3e7093aa04cd4604" + integrity sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw== + dependencies: + "@inquirer/ansi" "^2.0.7" + "@inquirer/core" "^11.2.1" + "@inquirer/figures" "^2.0.7" + "@inquirer/type" "^4.0.7" + +"@inquirer/type@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-4.0.7.tgz#9c6f0d857fe6ad549a3a932343b64e76acb34b10" + integrity sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g== + +"@napi-rs/cli@^3.6.0": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-3.7.2.tgz#4a43d7bc7703159da0de1a7e3b1cd94141132b10" + integrity sha512-shDW0Td/XZQpP04Yy+OsMt1ILMKGGkoLcy1zVAsSAK0fLfWm0Upgkmfs/NOV2ZhMQwkgpR3ZEdyHmTwgrUDQuA== + dependencies: + "@inquirer/prompts" "^8.5.2" + "@napi-rs/cross-toolchain" "^1.0.3" + "@napi-rs/wasm-tools" "^1.0.1" + "@octokit/rest" "^22.0.1" + clipanion "^4.0.0-rc.4" + colorette "^2.0.20" + emnapi "^1.11.1" + es-toolkit "^1.47.0" + js-yaml "^4.2.0" + obug "^2.1.2" + semver "^7.8.2" + typanion "^3.14.0" + +"@napi-rs/cross-toolchain@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz#8e345d0c9a8aeeaf9287e7af1d4ce83476681373" + integrity sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg== + dependencies: + "@napi-rs/lzma" "^1.4.5" + "@napi-rs/tar" "^1.1.0" + debug "^4.4.1" + +"@napi-rs/lzma-android-arm-eabi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.4.5.tgz#c6722a1d7201e269fdb6ba997d28cb41223e515c" + integrity sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q== + +"@napi-rs/lzma-android-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.4.5.tgz#05df61667e84419e0550200b48169057b734806f" + integrity sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ== + +"@napi-rs/lzma-darwin-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.4.5.tgz#c37a01c53f25cb7f014870d2ea6c5576138bcaaa" + integrity sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA== + +"@napi-rs/lzma-darwin-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.4.5.tgz#555b1dd65d7b104d28b2a12d925d7059226c7f4b" + integrity sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw== + +"@napi-rs/lzma-freebsd-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.4.5.tgz#683beff15b37774ec91e1de7b4d337894bf43694" + integrity sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw== + +"@napi-rs/lzma-linux-arm-gnueabihf@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.4.5.tgz#505f659a9131474b7270afa4a4e9caf709c4d213" + integrity sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw== + +"@napi-rs/lzma-linux-arm64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.4.5.tgz#ecbb944635fa004a9415d1f50f165bc0d26d3807" + integrity sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg== + +"@napi-rs/lzma-linux-arm64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.4.5.tgz#c0d17f40ce2db0b075469a28f233fd8ce31fbb95" + integrity sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w== + +"@napi-rs/lzma-linux-ppc64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.4.5.tgz#2f17b9d1fc920c6c511d2086c7623752172c2f07" + integrity sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ== + +"@napi-rs/lzma-linux-riscv64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.4.5.tgz#63c2a4e1157586252186e39604370d5b29c6db85" + integrity sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA== + +"@napi-rs/lzma-linux-s390x-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.4.5.tgz#6f2ca44bf5c5bef1b31d7516bf15d63c35cdf59f" + integrity sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA== + +"@napi-rs/lzma-linux-x64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.4.5.tgz#54879d88a9c370687b5463c7c1b6208b718c1ab2" + integrity sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw== + +"@napi-rs/lzma-linux-x64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.4.5.tgz#412705f6925f10f45122bd0f3e2fb6e597bed4f8" + integrity sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ== + +"@napi-rs/lzma-wasm32-wasi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.4.5.tgz#4b74abfd144371123cb6f5b7bad5bae868206ecf" + integrity sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/lzma-win32-arm64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.4.5.tgz#7ed8c80d588fa244a7fd55249cb0d011d04bf984" + integrity sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg== + +"@napi-rs/lzma-win32-ia32-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.4.5.tgz#e6f70ca87bd88370102aa610ee9e44ec28911b46" + integrity sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg== + +"@napi-rs/lzma-win32-x64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.4.5.tgz#ecfcfe364e805915608ce0ff41ed4c950fdb51b8" + integrity sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q== + +"@napi-rs/lzma@^1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma/-/lzma-1.4.5.tgz#43e17cdfe332a3f33fa640422da348db3d8825e1" + integrity sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg== + optionalDependencies: + "@napi-rs/lzma-android-arm-eabi" "1.4.5" + "@napi-rs/lzma-android-arm64" "1.4.5" + "@napi-rs/lzma-darwin-arm64" "1.4.5" + "@napi-rs/lzma-darwin-x64" "1.4.5" + "@napi-rs/lzma-freebsd-x64" "1.4.5" + "@napi-rs/lzma-linux-arm-gnueabihf" "1.4.5" + "@napi-rs/lzma-linux-arm64-gnu" "1.4.5" + "@napi-rs/lzma-linux-arm64-musl" "1.4.5" + "@napi-rs/lzma-linux-ppc64-gnu" "1.4.5" + "@napi-rs/lzma-linux-riscv64-gnu" "1.4.5" + "@napi-rs/lzma-linux-s390x-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-musl" "1.4.5" + "@napi-rs/lzma-wasm32-wasi" "1.4.5" + "@napi-rs/lzma-win32-arm64-msvc" "1.4.5" + "@napi-rs/lzma-win32-ia32-msvc" "1.4.5" + "@napi-rs/lzma-win32-x64-msvc" "1.4.5" + +"@napi-rs/tar-android-arm-eabi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.0.tgz#08ae6ebbaf38d416954a28ca09bf77410d5b0c2b" + integrity sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA== + +"@napi-rs/tar-android-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.0.tgz#825a76140116f89d7e930245bda9f70b196da565" + integrity sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw== + +"@napi-rs/tar-darwin-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.0.tgz#8821616c40ea52ec2c00a055be56bf28dee76013" + integrity sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw== + +"@napi-rs/tar-darwin-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.0.tgz#4a975e41932a145c58181cb43c8f483c3858e359" + integrity sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA== + +"@napi-rs/tar-freebsd-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.0.tgz#5ebc0633f257b258aacc59ac1420835513ed0967" + integrity sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g== + +"@napi-rs/tar-linux-arm-gnueabihf@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.0.tgz#1d309bd4f46f0490353d9608e79d260cf6c7cd43" + integrity sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg== + +"@napi-rs/tar-linux-arm64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.0.tgz#88d974821f3f8e9ee6948b4d51c78c019dee88ad" + integrity sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA== + +"@napi-rs/tar-linux-arm64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.0.tgz#ab2baee7b288df5e68cef0b2d12fa79d2a551b58" + integrity sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ== + +"@napi-rs/tar-linux-ppc64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.0.tgz#7500e60d27849ba36fa4802a346249974e7ecf74" + integrity sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ== + +"@napi-rs/tar-linux-s390x-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.0.tgz#cfc0923bfad1dea8ef9da22148a8d4932aa52d08" + integrity sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ== + +"@napi-rs/tar-linux-x64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.0.tgz#5fdf9e1bb12b10a951c6ab03268a9f8d9788c929" + integrity sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww== + +"@napi-rs/tar-linux-x64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.0.tgz#f001fc0a0a2996dcf99e787a15eade8dce215e91" + integrity sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ== + +"@napi-rs/tar-wasm32-wasi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.0.tgz#c1c7df7738b23f1cdbcff261d5bea6968d0a3c9a" + integrity sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/tar-win32-arm64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.0.tgz#4c8519eab28021e1eda0847433cab949d5389833" + integrity sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg== + +"@napi-rs/tar-win32-ia32-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.0.tgz#4f61af0da2c53b23f7d58c77970eaa4449e8eb79" + integrity sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ== + +"@napi-rs/tar-win32-x64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.0.tgz#eb63fb44ecde001cce6be238f175e66a06c15035" + integrity sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw== + +"@napi-rs/tar@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar/-/tar-1.1.0.tgz#acecd9e29f705a3f534d5fb3d8aa36b3266727d0" + integrity sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ== + optionalDependencies: + "@napi-rs/tar-android-arm-eabi" "1.1.0" + "@napi-rs/tar-android-arm64" "1.1.0" + "@napi-rs/tar-darwin-arm64" "1.1.0" + "@napi-rs/tar-darwin-x64" "1.1.0" + "@napi-rs/tar-freebsd-x64" "1.1.0" + "@napi-rs/tar-linux-arm-gnueabihf" "1.1.0" + "@napi-rs/tar-linux-arm64-gnu" "1.1.0" + "@napi-rs/tar-linux-arm64-musl" "1.1.0" + "@napi-rs/tar-linux-ppc64-gnu" "1.1.0" + "@napi-rs/tar-linux-s390x-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-musl" "1.1.0" + "@napi-rs/tar-wasm32-wasi" "1.1.0" + "@napi-rs/tar-win32-arm64-msvc" "1.1.0" + "@napi-rs/tar-win32-ia32-msvc" "1.1.0" + "@napi-rs/tar-win32-x64-msvc" "1.1.0" + "@napi-rs/wasm-runtime@^0.2.11": version "0.2.12" resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2" @@ -114,6 +494,194 @@ "@emnapi/runtime" "^1.4.3" "@tybys/wasm-util" "^0.10.0" +"@napi-rs/wasm-runtime@^1.0.3": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz#cccd6ebc40b991dea6936f9126b1b8155b6c4c95" + integrity sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q== + dependencies: + "@tybys/wasm-util" "^0.10.2" + +"@napi-rs/wasm-tools-android-arm-eabi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz#a709f93ddd95508a4ef949b5ceff2b2e85b676f7" + integrity sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw== + +"@napi-rs/wasm-tools-android-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz#304b5761b4fcc871b876ebd34975c72c9d11a7fc" + integrity sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg== + +"@napi-rs/wasm-tools-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz#dafb4330986a8b46e8de1603ea2f6932a19634c6" + integrity sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g== + +"@napi-rs/wasm-tools-darwin-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz#0919e63714ee0a52b1120f6452bbc3a4d793ce3c" + integrity sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A== + +"@napi-rs/wasm-tools-freebsd-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz#1f50a2d5d5af041c55634f43f623ae49192bce9c" + integrity sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg== + +"@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz#6106d5e65a25ec2ae417c2fcfebd5c8f14d80e84" + integrity sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q== + +"@napi-rs/wasm-tools-linux-arm64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz#0eb3d4d1fbc1938b0edd907423840365ebc53859" + integrity sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ== + +"@napi-rs/wasm-tools-linux-x64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz#5de6a567083a83efed16d046f47b680cbe7c9b53" + integrity sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw== + +"@napi-rs/wasm-tools-linux-x64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz#04cc17ef12b4e5012f2d0e46b09cabe473566e5a" + integrity sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ== + +"@napi-rs/wasm-tools-wasm32-wasi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz#6ced3bd03428c854397f00509b1694c3af857a0f" + integrity sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz#e776f66eb637eee312b562e987c0a5871ddc6dac" + integrity sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ== + +"@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz#9167919a62d24cb3a46f01fada26fee38aeaf884" + integrity sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw== + +"@napi-rs/wasm-tools-win32-x64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz#f896ab29a83605795bb12cf2cfc1a215bc830c65" + integrity sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ== + +"@napi-rs/wasm-tools@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz#f54caa0132322fd5275690b2aeb581d11539262f" + integrity sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ== + optionalDependencies: + "@napi-rs/wasm-tools-android-arm-eabi" "1.0.1" + "@napi-rs/wasm-tools-android-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-x64" "1.0.1" + "@napi-rs/wasm-tools-freebsd-x64" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-musl" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-musl" "1.0.1" + "@napi-rs/wasm-tools-wasm32-wasi" "1.0.1" + "@napi-rs/wasm-tools-win32-arm64-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-ia32-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-x64-msvc" "1.0.1" + +"@octokit/auth-token@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" + integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== + +"@octokit/core@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" + integrity sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q== + dependencies: + "@octokit/auth-token" "^6.0.0" + "@octokit/graphql" "^9.0.3" + "@octokit/request" "^10.0.6" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + before-after-hook "^4.0.0" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^11.0.3": + version "11.0.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.3.tgz#acf5f7feddde4e12185d5312ee38ff77235d8205" + integrity sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag== + dependencies: + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" + integrity sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA== + dependencies: + "@octokit/request" "^10.0.6" + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^27.0.0": + version "27.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" + integrity sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA== + +"@octokit/plugin-paginate-rest@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz#44dc9fff2dacb148d4c5c788b573ddc044503026" + integrity sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/plugin-request-log@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" + integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== + +"@octokit/plugin-rest-endpoint-methods@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" + integrity sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request-error@^7.0.2": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" + integrity sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request@^10.0.6": + version "10.0.10" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.10.tgz#45e46934f3d772f006733be6b5ec18f22e54a00c" + integrity sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w== + dependencies: + "@octokit/endpoint" "^11.0.3" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + content-type "^2.0.0" + json-with-bigint "^3.5.3" + universal-user-agent "^7.0.2" + +"@octokit/rest@^22.0.1": + version "22.0.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.1.tgz#4d866c32b76b711d3f736f91992e2b534163b416" + integrity sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw== + dependencies: + "@octokit/core" "^7.0.6" + "@octokit/plugin-paginate-rest" "^14.0.0" + "@octokit/plugin-request-log" "^6.0.0" + "@octokit/plugin-rest-endpoint-methods" "^17.0.0" + +"@octokit/types@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" + integrity sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg== + dependencies: + "@octokit/openapi-types" "^27.0.0" + "@package-json/types@^0.0.12": version "0.0.12" resolved "https://registry.yarnpkg.com/@package-json/types/-/types-0.0.12.tgz#4629e833ba128ed9880b6b7a947633ee22952462" @@ -138,6 +706,13 @@ dependencies: tslib "^2.4.0" +"@tybys/wasm-util@^0.10.2": + version "0.10.2" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz#12b3a1b33db1f9cad4ddff1f604ab7dd00bf464e" + integrity sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg== + dependencies: + tslib "^2.4.0" + "@types/esrecurse@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@types/esrecurse/-/esrecurse-4.3.1.tgz#6f636af962fbe6191b830bd676ba5986926bccec" @@ -275,6 +850,11 @@ ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + balanced-match@^4.0.2: version "4.0.4" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" @@ -285,6 +865,11 @@ baseline-browser-mapping@^2.9.0: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== +before-after-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" + integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== + brace-expansion@^5.0.2: version "5.0.4" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" @@ -318,6 +903,11 @@ change-case@^5.4.4: resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== +chardet@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.1.tgz#5c75593704a642f71ee53717df234031e65373c8" + integrity sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ== + ci-info@^4.3.1: version "4.4.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.4.0.tgz#7d54eff9f54b45b62401c26032696eb59c8bd18c" @@ -330,11 +920,33 @@ clean-regexp@^1.0.0: dependencies: escape-string-regexp "^1.0.5" +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + +clipanion@^4.0.0-rc.4: + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-4.0.0-rc.4.tgz#7191a940e47ef197e5f18c9cbbe419278b5f5903" + integrity sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q== + dependencies: + typanion "^3.8.0" + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + comment-parser@^1.4.1: version "1.4.5" resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.5.tgz#6c595cd090737a1010fe5ff40d86e1d21b7bd6ce" integrity sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw== +content-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-2.0.0.tgz#2fb3ede69dffa0af78ca7c4ce7589680638b56df" + integrity sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ== + core-js-compat@^3.46.0: version "3.48.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6" @@ -368,6 +980,11 @@ electron-to-chromium@^1.5.263: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz#09f8973100c39fb0d003b890393cd1d58932b1c8" integrity sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg== +emnapi@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/emnapi/-/emnapi-1.11.1.tgz#9f3da4463d61a89822ac902f95a3e324adfd340e" + integrity sha512-kSRjhIcxjMFsBqk7ORvoc9aA5SBKDmecrtF5RMcmOTao0kD/zamaxsuTxMI8C1//wGUuvE7a+19pCE7AEhGVnA== + enhanced-resolve@^5.17.1: version "5.20.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz#323c2a70d2aa7fb4bdfd6d3c24dfc705c581295d" @@ -376,6 +993,11 @@ enhanced-resolve@^5.17.1: graceful-fs "^4.2.4" tapable "^2.3.0" +es-toolkit@^1.47.0: + version "1.47.1" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.47.1.tgz#6f049fef04c2ef27400e8f38255a0dca323e1c3a" + integrity sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q== + escalade@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" @@ -586,6 +1208,25 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-string-truncated-width@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz#23afe0da67d752ca0727538f1e6967759728ce49" + integrity sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g== + +fast-string-width@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/fast-string-width/-/fast-string-width-3.0.2.tgz#16dbabb491ce5585b5ecb675b65c165d71688eeb" + integrity sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg== + dependencies: + fast-string-truncated-width "^3.0.2" + +fast-wrap-ansi@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz#95e952a0145bce3f59ad56e179f84c48d4072935" + integrity sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q== + dependencies: + fast-string-width "^3.0.2" + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -658,6 +1299,13 @@ graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +iconv-lite@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.0, ignore@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -697,6 +1345,13 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +js-yaml@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.2.0.tgz#2bd9e85682dd91bd469afb809d816043b3d49524" + integrity sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw== + dependencies: + argparse "^2.0.1" + jsesc@^3.1.0, jsesc@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" @@ -717,6 +1372,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +json-with-bigint@^3.5.3: + version "3.5.8" + resolved "https://registry.yarnpkg.com/json-with-bigint/-/json-with-bigint-3.5.8.tgz#1b1edb55a1bc4816ca87ac684297591acd822383" + integrity sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw== + keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -751,6 +1411,11 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +mute-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-3.0.0.tgz#cd8014dd2acb72e1e91bb67c74f0019e620ba2d1" + integrity sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw== + napi-postinstall@^0.3.0: version "0.3.4" resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz#7af256d6588b5f8e952b9190965d6b019653bbb9" @@ -766,6 +1431,11 @@ node-releases@^2.0.27: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== +obug@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.3.tgz#c02c60f95abd603409330e767db7f2823193331e" + integrity sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -844,11 +1514,21 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + semver@^7.5.4, semver@^7.6.3, semver@^7.7.2, semver@^7.7.3: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@^7.8.2: + version "7.8.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.4.tgz#c73eceebae0616934be8dff28a7fd70757c8e696" + integrity sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -861,6 +1541,11 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + stable-hash-x@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz#dfd76bfa5d839a7470125c6a6b3c8b22061793e9" @@ -888,6 +1573,11 @@ tslib@^2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== +typanion@^3.14.0, typanion@^3.8.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.14.0.tgz#a766a91810ce8258033975733e836c43a2929b94" + integrity sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -895,6 +1585,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be" + integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A== + unrs-resolver@^1.9.2: version "1.11.1" resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz#be9cd8686c99ef53ecb96df2a473c64d304048a9" From e4e3fb201cef07155fc66dbb1a640a3e9f1c69a7 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Tue, 23 Jun 2026 16:33:45 -0400 Subject: [PATCH 02/12] feat(capabilities): add wasm capability bundle Implement the portable libdatadog capability traits for the wasm runtime: an HTTP client backed by Node's http.request, a setTimeout-based sleep, and a response-header observer hook. This bundle is the generic parameter the data-pipeline and trace-exporter crates are instantiated with. Co-authored-by: Jules Wiriath Co-authored-by: paullegranddc Co-authored-by: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> --- crates/capabilities/Cargo.toml | 21 +++ crates/capabilities/src/http.rs | 216 ++++++++++++++++++++++ crates/capabilities/src/http_transport.js | 101 ++++++++++ crates/capabilities/src/lib.rs | 18 ++ test/http_transport.js | 115 ++++++++++++ 5 files changed, 471 insertions(+) create mode 100644 crates/capabilities/Cargo.toml create mode 100644 crates/capabilities/src/http.rs create mode 100644 crates/capabilities/src/http_transport.js create mode 100644 crates/capabilities/src/lib.rs create mode 100644 test/http_transport.js diff --git a/crates/capabilities/Cargo.toml b/crates/capabilities/Cargo.toml new file mode 100644 index 0000000..d20be8f --- /dev/null +++ b/crates/capabilities/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "libdatadog-nodejs-capabilities" +version = "0.1.0" +edition = "2021" +description = "Wasm capability implementations for libdatadog-nodejs (backed by JS transports)" + +[lib] +crate-type = ["rlib"] + +[dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +http = "1" +bytes = "1.4" +futures-core = "0.3" +anyhow = "1" +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/capabilities/src/http.rs b/crates/capabilities/src/http.rs new file mode 100644 index 0000000..5955b34 --- /dev/null +++ b/crates/capabilities/src/http.rs @@ -0,0 +1,216 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm implementation of [`HttpClientTrait`] backed by Node.js `http.request`. +//! +//! The JS transport is imported via `wasm_bindgen(module = ...)` from +//! `http_transport.js`, which ships alongside the wasm output. + +use std::future::Future; +use std::io::Write as _; +use std::sync::LazyLock; +use std::time::Duration; + +use bytes::Bytes; +use http::{HeaderMap, HeaderName, HeaderValue}; +use js_sys::{self, Array, JsString, Number, Uint8Array}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::JsFuture; + +use libdd_capabilities::http::{HttpClientCapability, HttpError}; +use libdd_capabilities::maybe_send::MaybeSend; +use libdd_capabilities::sleep::SleepCapability; + +static WASM_MEMORY: LazyLock = LazyLock::new(|| wasm_bindgen::memory()); + +#[wasm_bindgen(module = "/src/http_transport.js")] +extern "C" { + #[wasm_bindgen(js_name = "httpRequest")] + fn http_request( + host: &str, + port: u16, + is_https: bool, + head_ptr: *const u8, + head_len: u32, + body_ptr: *const u8, + body_len: u32, + wasm_memory: &JsValue, + ) -> js_sys::Promise; + + #[wasm_bindgen(js_name = "sleep")] + fn js_sleep(ms: f64) -> js_sys::Promise; + + #[wasm_bindgen(js_name = "setStorage")] + pub fn set_storage(new_storage: &JsValue); + + #[wasm_bindgen(js_name = "setResponseHeaderObserver")] + pub fn set_response_header_observer(observer: &JsValue); +} + +/// Wasm [`HttpClientTrait`] implementation that delegates to Node.js HTTP. +/// +/// Named `DefaultHttpClient` to match the native version's public API. +#[derive(Debug, Clone)] +pub struct DefaultHttpClient; + +impl HttpClientCapability for DefaultHttpClient { + fn new_client() -> Self { + Self + } + + #[allow(clippy::manual_async_fn)] + fn request( + &self, + req: http::Request, + ) -> impl Future, HttpError>> + MaybeSend { + async move { + let scheme = req.uri().scheme_str().unwrap_or("http"); + let is_https = scheme == "https"; + let host = req + .uri() + .host() + .ok_or_else(|| HttpError::InvalidRequest(anyhow::anyhow!("missing host in URI")))? + .to_owned(); + let port = req + .uri() + .port_u16() + .unwrap_or(if is_https { 443 } else { 80 }); + + let head = serialize_request_head(&req, &host, port, is_https)?; + let body = req.into_body(); + + let result = JsFuture::from(http_request( + &host, + port, + is_https, + head.as_ptr(), + head.len() as u32, + body.as_ptr(), + body.len() as u32, + WASM_MEMORY.as_ref(), + )) + .await + .map_err(|e| HttpError::Network(anyhow::anyhow!("{:?}", e)))?; + + let result: js_sys::ArrayTuple<(Number, Array, Uint8Array)> = + js_sys::ArrayTuple::unchecked_from_js(result); + + let status = result + .get0() + .as_f64() + .ok_or_else(|| HttpError::Other(anyhow::anyhow!("status is not a number")))? + as u16; + + let headers = parse_response_headers(result.get1())?; + + let body = Bytes::from(result.get2().to_vec()); + + let mut builder = http::Response::builder().status(status); + *builder.headers_mut().unwrap() = headers; + builder.body(body).map_err(|e| HttpError::Other(e.into())) + } + } +} + +/// Wasm [`SleepCapability`] backed by JS `setTimeout`. +/// +/// TraceExporter requires its capability bundle to implement `SleepCapability` +/// (used for retry backoff). Native code uses `tokio::time::sleep`; in wasm we +/// delegate to `setTimeout` via a JS-returned Promise. +impl SleepCapability for DefaultHttpClient { + fn new() -> Self { + Self + } + + #[allow(clippy::manual_async_fn)] + fn sleep(&self, duration: Duration) -> impl Future + MaybeSend { + async move { + let ms = duration.as_millis() as f64; + let _ = JsFuture::from(js_sleep(ms)).await; + } + } +} + +/// Parse response headers from a JS object `{ "header-name": "value", ... }`. +/// +/// Node.js `res.headers` returns lowercased header names with string values. +fn parse_response_headers(header_js: Array) -> Result { + let len = header_js.length() as usize; + let mut headers = HeaderMap::with_capacity(len / 2); + for i in 0..(len / 2) { + let key = header_js.get((i * 2) as u32).as_string(); + let val = header_js.get((i * 2 + 1) as u32).as_string(); + if let (Some(key), Some(val)) = (key, val) { + // Response headers come from the agent (untrusted over plaintext + // HTTP); skip any the http crate rejects rather than unwrapping + // and trapping the whole wasm instance on one malformed header. + if let (Ok(name), Ok(value)) = ( + HeaderName::from_bytes(key.as_bytes()), + HeaderValue::from_maybe_shared(Bytes::from(val)), + ) { + headers.insert(name, value); + } + } + } + Ok(headers) +} + +/// Serialize the full HTTP/1.1 request head (request line + Host + Content-Length +/// + user headers + terminating CRLF) into a contiguous byte buffer. +/// +/// The buffer is handed to JS by pointer; JS assigns it to +/// `req._header`, bypassing Node's `_storeHeader` serialization. +fn serialize_request_head( + req: &http::Request, + host: &str, + port: u16, + is_https: bool, +) -> Result, HttpError> { + let method = req.method().as_str(); + let path_and_query = req + .uri() + .path_and_query() + .map(|pq| pq.as_str()) + .unwrap_or("/"); + let body_len = req.body().len(); + let headers = req.headers(); + + let mut buf = Vec::with_capacity(256 + headers.len() * 64); + + buf.extend_from_slice(method.as_bytes()); + buf.push(b' '); + buf.extend_from_slice(path_and_query.as_bytes()); + buf.extend_from_slice(b" HTTP/1.1\r\n"); + + buf.extend_from_slice(b"Host: "); + buf.extend_from_slice(host.as_bytes()); + let default_port = if is_https { 443 } else { 80 }; + if port != default_port { + write!(&mut buf, ":{port}").map_err(|e| HttpError::Other(e.into()))?; + } + buf.extend_from_slice(b"\r\n"); + + write!(&mut buf, "Content-Length: {body_len}\r\n").map_err(|e| HttpError::Other(e.into()))?; + + for (name, value) in headers.iter() { + // The request-framing headers above (Host, Content-Length) are + // authoritative. Skip any caller-supplied duplicates of them (and + // Transfer-Encoding) so they can't be emitted twice — duplicate/ + // conflicting framing headers are a request-smuggling vector when the + // agent URL is reached through a proxy. + if matches!( + name.as_str(), + "host" | "content-length" | "transfer-encoding" + ) { + continue; + } + buf.extend_from_slice(name.as_str().as_bytes()); + buf.extend_from_slice(b": "); + buf.extend_from_slice(value.as_bytes()); + buf.extend_from_slice(b"\r\n"); + } + + buf.extend_from_slice(b"\r\n"); + + Ok(buf) +} diff --git a/crates/capabilities/src/http_transport.js b/crates/capabilities/src/http_transport.js new file mode 100644 index 0000000..bbc8267 --- /dev/null +++ b/crates/capabilities/src/http_transport.js @@ -0,0 +1,101 @@ +const http = require('http'); +const https = require('https'); + +let storage = (f) => f(); + +module.exports.sleep = function (ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + +module.exports.setStorage = function (new_storage) { + storage = new_storage; +} + +// Optional observer invoked with each agent response's raw headers +// (Node's flat [name, value, name, value, ...] array). Lets the host tracer +// read response-only headers (e.g. Datadog-Container-Tags-Hash) that are not +// otherwise surfaced through the wasm response body. Never throws into the +// transport: a misbehaving observer must not break trace delivery. +// +// The observer runs synchronously on the response 'end' event, so it must be +// non-blocking and return quickly — long-running synchronous work here would +// stall the event loop. +let responseHeaderObserver = null; + +module.exports.setResponseHeaderObserver = function (new_observer) { + responseHeaderObserver = new_observer; +} + +module.exports.httpRequest = function (host, port, isHttps, head_ptr, head_len, body_ptr, body_len, wasm_memory) { + const transport = isHttps ? https : http; + + function isDetachedBufferError(err) { + return err instanceof TypeError && /detached/i.test(err.message); + } + + function attempt() { + return new Promise((resolve, reject) => { + storage(() => { + // wasm_memory.buffer is replaced each time WebAssembly.Memory grows, so + // the views must be recreated on every attempt against the current buffer. + const headView = new Uint8Array(wasm_memory.buffer, head_ptr, head_len); + const bodyView = new Uint8Array(wasm_memory.buffer, body_ptr, body_len); + + // host/port drive socket selection; method/path/headers are placeholders + // because we replace the rendered head below. + const req = transport.request({ host, port, method: 'POST', path: '/' }, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const body = Buffer.concat(chunks) + if (responseHeaderObserver !== null) { + try { + responseHeaderObserver(res.rawHeaders); + } catch (err) { + // Only read `err.message` (a string) rather than stringifying an + // arbitrary thrown value, so a hostile/throwing toString on the + // error can't turn the log line into its own failure path. + process.stderr.write("responseHeaderObserver error: " + (err && err.message) + "\n"); + } + } + resolve([ + res.statusCode, + res.rawHeaders, + // Copy the exact body bytes. `body` is a Buffer from Buffer.concat, + // which for small payloads is a view into Node's shared pool, so + // `body.buffer` is the whole pool — slicing by offset/length (via + // the Uint8Array(typedArray) copy ctor) is required to avoid + // handing the Rust side unrelated pooled memory. + new Uint8Array(body), + ]); + }); + }); + req.on('error', reject); + + // Bypass Node's headers: the Rust side has already produced the full + // request head in HTTP/1.1 wire format. Setting _header before write() + // makes write/end skip _implicitHeader and _send prepends our bytes. + + try { + req._header = Buffer.from(headView); + req.write(bodyView); + req.end(); + } catch (err) { + reject(err); + } + }) + }); + } + + function attemptWithRetry() { + return attempt().catch((err) => { + process.stderr.write("httpRequest error: " + err + "\n") + if (isDetachedBufferError(err)) { + return attemptWithRetry(); + } + throw err; + }); + } + + return attemptWithRetry(); +}; diff --git a/crates/capabilities/src/lib.rs b/crates/capabilities/src/lib.rs new file mode 100644 index 0000000..a6f4deb --- /dev/null +++ b/crates/capabilities/src/lib.rs @@ -0,0 +1,18 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm capability implementations for libdatadog-nodejs. +//! +//! `WasmCapabilities` is the bundle struct that implements all capability +//! traits using wasm_bindgen + JS transports. The wasm binding crate pins +//! this type as the generic parameter for libdatadog structs. + +pub mod http; + +pub use http::DefaultHttpClient; + +/// Bundle struct for wasm platform capabilities. +/// +/// Currently delegates to `DefaultHttpClient` for HTTP. As more capability +/// traits are added (spawn, sleep, etc.), this type will implement all of them. +pub type WasmCapabilities = DefaultHttpClient; diff --git a/test/http_transport.js b/test/http_transport.js new file mode 100644 index 0000000..762f5c5 --- /dev/null +++ b/test/http_transport.js @@ -0,0 +1,115 @@ +'use strict' + +// Unit tests for the response-header observer hook in the WASM HTTP transport +// shim. The shim is plain CommonJS (no wasm needed), so we drive `httpRequest` +// directly against a local HTTP server. `httpRequest` reads the request head +// from a Uint8Array view over `wasm_memory.buffer`, so we hand it a fake memory +// object containing a well-formed HTTP/1.1 request head. + +const { describe, it, before, after, beforeEach } = require('node:test') +const assert = require('node:assert') +const http = require('node:http') + +const transport = require('../crates/capabilities/src/http_transport.js') + +// Distinctive, multi-byte body so the pooled-buffer slicing in httpRequest +// (the reason for `new Uint8Array(body)` over `body.buffer`) is exercised: +// a small Buffer.concat result lands at a non-zero offset in Node's shared pool. +const RESPONSE_BODY = '{"rate_by_service":{"service:test,env:":0.5}}' + +function fakeWasmMemory (headBytes) { + const buf = new ArrayBuffer(headBytes.length) + new Uint8Array(buf).set(headBytes) + return { buffer: buf } +} + +describe('http_transport response header observer', () => { + let server + let port + + before(async () => { + server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + res.setHeader('Datadog-Container-Tags-Hash', 'testhash123') + res.end(RESPONSE_BODY) + }) + }) + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)) + port = server.address().port + }) + + after(() => new Promise((resolve) => server.close(resolve))) + + beforeEach(() => { + transport.setResponseHeaderObserver(null) + }) + + function doRequest () { + const head = Buffer.from( + `POST /v0.4/traces HTTP/1.1\r\nHost: 127.0.0.1:${port}\r\n` + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + 'utf8' + ) + // head occupies [0, head.length); body is empty (offset 0, length 0). + return transport.httpRequest('127.0.0.1', port, false, 0, head.length, 0, 0, fakeWasmMemory(head)) + } + + it('invokes the observer with the raw response headers', async () => { + let observed + transport.setResponseHeaderObserver((rawHeaders) => { observed = rawHeaders }) + + await doRequest() + + assert.ok(Array.isArray(observed), 'observer received the raw headers array') + const idx = observed.findIndex((h) => h.toLowerCase() === 'datadog-container-tags-hash') + assert.notStrictEqual(idx, -1, 'container-tags hash header present') + assert.strictEqual(observed[idx + 1], 'testhash123') + }) + + it('still delivers the response when the observer throws, logging the error', async () => { + transport.setResponseHeaderObserver(() => { throw new Error('boom') }) + + const originalWrite = process.stderr.write + let logged = '' + process.stderr.write = (chunk) => { logged += chunk; return true } + try { + const [status] = await doRequest() + assert.strictEqual(status, 200) + } finally { + process.stderr.write = originalWrite + } + assert.match(logged, /responseHeaderObserver error: boom/) + }) + + it('tolerates an observer throwing a non-Error value', async () => { + // Hardened logging reads only err.message, so a thrown string must not + // crash the transport (it logs `undefined` for the missing message). + transport.setResponseHeaderObserver(() => { throw 'boom' }) // eslint-disable-line no-throw-literal + + const originalWrite = process.stderr.write + let logged = '' + process.stderr.write = (chunk) => { logged += chunk; return true } + try { + const [status] = await doRequest() + assert.strictEqual(status, 200) + } finally { + process.stderr.write = originalWrite + } + assert.match(logged, /responseHeaderObserver error: undefined/) + }) + + it('works when no observer is registered', async () => { + const [status] = await doRequest() + assert.strictEqual(status, 200) + }) + + it('returns the exact response body bytes', async () => { + const [status, , body] = await doRequest() + assert.strictEqual(status, 200) + assert.ok(body instanceof Uint8Array, 'body is a Uint8Array') + // Must be exactly the agent's body — not whole-pool bytes or wrong length. + assert.strictEqual(body.length, Buffer.byteLength(RESPONSE_BODY)) + assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY) + }) +}) From c53277be173e8842f514120d9d716d1cb2bc51b5 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Tue, 23 Jun 2026 16:33:45 -0400 Subject: [PATCH 03/12] feat(pipeline): add native-spans wasm pipeline Wasm binding over libdatadog's span-id-addressed change buffer: span creation and mutation via a binary op protocol, a deduplicated string table, segment (trace-level) attributes, batched export through prepareChunk/sendPreparedChunk, and optional client-side stats. Co-authored-by: Jules Wiriath Co-authored-by: paullegranddc Co-authored-by: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> --- crates/pipeline/Cargo.toml | 32 ++ crates/pipeline/src/lib.rs | 544 ++++++++++++++++++++++ crates/pipeline/src/span_bytes.rs | 25 ++ crates/pipeline/src/span_string.rs | 46 ++ crates/pipeline/src/stats.rs | 149 ++++++ crates/pipeline/src/trace_data.rs | 12 + crates/pipeline/src/utils.rs | 37 ++ test/pipeline.js | 699 ++++++++++++++++++++++++++++- 8 files changed, 1540 insertions(+), 4 deletions(-) create mode 100644 crates/pipeline/Cargo.toml create mode 100644 crates/pipeline/src/lib.rs create mode 100644 crates/pipeline/src/span_bytes.rs create mode 100644 crates/pipeline/src/span_string.rs create mode 100644 crates/pipeline/src/stats.rs create mode 100644 crates/pipeline/src/trace_data.rs create mode 100644 crates/pipeline/src/utils.rs diff --git a/crates/pipeline/Cargo.toml b/crates/pipeline/Cargo.toml new file mode 100644 index 0000000..f7d9935 --- /dev/null +++ b/crates/pipeline/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "pipeline" +version = "0.1.0" +edition = "2021" +description = "Wasm binding for pipeline span management and trace export" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +libdatadog-nodejs-capabilities = { path = "../capabilities" } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } +libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-trace-utils = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false, features = ["change-buffer"] } +libdd-trace-stats = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-trace-protobuf = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +rmp-serde = "1" +bytes = "1" +http = "1" +console_error_panic_hook = "0.1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1", features = ["js"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs new file mode 100644 index 0000000..5a2b7b3 --- /dev/null +++ b/crates/pipeline/src/lib.rs @@ -0,0 +1,544 @@ +use libdatadog_nodejs_capabilities::WasmCapabilities; +use libdd_data_pipeline::trace_exporter::agent_response::AgentResponse; +use libdd_data_pipeline::trace_exporter::{TraceExporter, TraceExporterBuilder}; +use libdd_shared_runtime::LocalRuntime; +use std::cell::{Cell, RefCell, UnsafeCell}; +use std::ffi::CStr; +use std::time::Duration; + +use wasm_bindgen::prelude::*; + +mod span_string; + +mod span_bytes; + +mod trace_data; +use trace_data::*; + +mod stats; + +use libdd_trace_utils::change_buffer::{ChangeBuffer, ChangeBufferState}; + +mod utils; +use utils::*; + +#[wasm_bindgen(start)] +fn init() { + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +/// All mutable state is behind RefCell to allow `&self` methods on the +/// wasm-bindgen wrapper. This prevents re-entrant borrow panics when: +/// - Plugin instrumentation triggers span creation inside another span's +/// creation (e.g., http client span created during express handler) +/// - Async `flushChunk` holds a borrow across await points while other +/// span operations need access +pub struct WasmSpanState { + change_queue: Vec, + string_table_input: Vec, + /// UnsafeCell because send_trace_chunks_async needs &mut self across an + /// await point. WASM is single-threaded so this is safe — we just need + /// to ensure no overlapping mutable borrows (guaranteed by the JS-side + /// _flushInFlight guard which serializes sendPreparedChunk calls). + /// On wasm the exporter must be built asynchronously (`build_async`), but + /// the wasm-bindgen constructor is synchronous. We stash the configured + /// builder here and build the exporter lazily on the first send (the only + /// path that needs it), inside an async context. + // On wasm the exporter runs on a single-threaded `LocalRuntime` (workers + // spawned via wasm_bindgen_futures::spawn_local); the multi-thread + // ForkSafeRuntime/BasicRuntime are native-only. + exporter: UnsafeCell>>, + builder: UnsafeCell>>, + cbs: RefCell>, + stats_collector: RefCell>, + prepared_spans: RefCell>>>, + /// Re-entrancy guard for `sendPreparedChunk`. wasm-bindgen async exports + /// can be invoked again from JS before the prior future resolves; without + /// this, two calls would each take `&mut` out of `exporter`/`builder` and + /// alias across the await (UB). The guard makes a re-entrant call return + /// an error instead. + sending: Cell, +} + +/// Clears an in-flight flag on drop, so an early return or a dropped future +/// still resets it. +struct InFlightGuard<'a>(&'a Cell); +impl Drop for InFlightGuard<'_> { + fn drop(&mut self) { + self.0.set(false); + } +} + +#[wasm_bindgen] +impl WasmSpanState { + #[wasm_bindgen(constructor)] + pub fn new( + url: &str, + tracer_version: &str, + lang: &str, + lang_version: &str, + lang_interpreter: &str, + change_queue_size: u32, + string_table_input_size: u32, + pid: u32, + tracer_service: &str, + stats_enabled: bool, + hostname: &str, + env: &str, + app_version: &str, + runtime_id: &str, + ) -> Result { + let mut builder = TraceExporterBuilder::::new(); + builder + .set_url(url) + .set_tracer_version(tracer_version) + .set_language(lang) + .set_language_version(lang_version) + .set_language_interpreter(lang_interpreter) + // Populate the payload-level TracerMetadata (service/env/hostname/ + // app_version) the agent receives. These values are already passed + // in for the stats collector; without these calls the trace + // payload's tracer metadata is sent empty. + .set_service(tracer_service) + .set_env(env) + .set_hostname(hostname) + .set_app_version(app_version) + .enable_agent_rates_payload_version(); + + let mut change_queue = vec![0u8; change_queue_size as usize]; + let change_buffer = unsafe { + ChangeBuffer::from_raw_parts( + std::ptr::NonNull::new(change_queue.as_mut_ptr()).unwrap(), + change_queue.len(), + ) + }; + let change_buffer_state = ChangeBufferState::new( + change_buffer, + tracer_service.into(), + lang.into(), + pid, + ); + + let stats_collector = if stats_enabled { + Some(stats::StatsCollector::new( + Duration::from_secs(10), + url.to_string(), + stats::StatsMeta { + hostname: hostname.to_string(), + env: env.to_string(), + version: app_version.to_string(), + lang: lang.to_string(), + tracer_version: tracer_version.to_string(), + runtime_id: runtime_id.to_string(), + service: tracer_service.to_string(), + }, + )) + } else { + None + }; + + Ok(WasmSpanState { + change_queue, + string_table_input: vec![0u8; string_table_input_size as usize], + exporter: UnsafeCell::new(None), + builder: UnsafeCell::new(Some(builder)), + cbs: RefCell::new(change_buffer_state), + stats_collector: RefCell::new(stats_collector), + prepared_spans: RefCell::new(None), + sending: Cell::new(false), + }) + } + + #[wasm_bindgen] + pub fn change_queue_ptr(&self) -> *const u8 { + self.change_queue.as_ptr() + } + + #[wasm_bindgen] + pub fn change_queue_len(&self) -> u32 { + self.change_queue.len() as u32 + } + + #[wasm_bindgen] + pub fn string_table_input_ptr(&self) -> *const u8 { + self.string_table_input.as_ptr() + } + + #[wasm_bindgen] + pub fn string_table_input_len(&self) -> u32 { + self.string_table_input.len() as u32 + } + + /// Prepare a chunk of spans for sending. Flushes the change buffer, + /// extracts spans, feeds stats. Returns `true` if a chunk was prepared + /// (there are spans to send) and `false` if there was nothing to send. + /// Must be followed by `sendPreparedChunk()` to actually send. + #[wasm_bindgen(js_name = "prepareChunk")] + pub fn prepare_chunk( + &self, + len: u32, + first_is_local_root: bool, + chunk: &[u8], + ) -> Result { + // Validate the JS-supplied count against the actual buffer size before + // doing any work: each span id is a u64 (8 bytes). This prevents an + // out-of-bounds read panic (and a huge `Vec::with_capacity`) when the + // caller passes a `len` larger than the chunk can hold. + if (len as usize).saturating_mul(8) > chunk.len() { + return Err(JsValue::from_str( + "prepareChunk: len exceeds the span-id bytes available in chunk", + )); + } + if len == 0 { + // Nothing to send: drop any previously prepared-but-unsent chunk so + // a caller that ignores this `false` cannot later resend a stale one. + if let Some(old_spans) = self.prepared_spans.borrow_mut().take() { + self.cbs.borrow_mut().recycle_spans(old_spans); + } + return Ok(false); + } + + self.cbs.borrow_mut() + .flush_change_buffer() + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + let mut count = len; + let mut index = 0; + let mut span_ids = Vec::with_capacity(count as usize); + while count > 0 { + let span_id: u64 = get_num(chunk, &mut index); + span_ids.push(span_id); + count -= 1; + } + + let spans_vec = self + .cbs.borrow_mut() + .flush_chunk(&span_ids, first_is_local_root) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + if let Some(collector) = self.stats_collector.borrow_mut().as_mut() { + collector.add_spans(&spans_vec); + } + + // Recycle any previously prepared spans that were never sent (e.g. + // if the prior send was skipped by JS back-pressure). Reusing the + // pre-allocated HashMaps avoids allocator fragmentation in WASM. + if let Some(old_spans) = self.prepared_spans.borrow_mut().take() { + self.cbs.borrow_mut().recycle_spans(old_spans); + } + + // Store prepared spans for the subsequent sendPreparedChunk call + let has_spans = !spans_vec.is_empty(); + *self.prepared_spans.borrow_mut() = Some(spans_vec); + Ok(has_spans) + } + + /// Send the previously prepared chunk. + /// + /// Uses `&self` (not `&mut self`); exclusive access to the exporter is + /// enforced at runtime by the `sending` re-entrancy guard below rather + /// than by the borrow checker. WASM is single-threaded, so the only way + /// two `&mut` to the exporter could co-exist is async re-entrancy (JS + /// calling this again before the prior future resolves) — the guard + /// rejects that with an error instead of allowing aliasing (UB). + #[wasm_bindgen(js_name = "sendPreparedChunk")] + pub async fn send_prepared_chunk(&self) -> Result { + if self.sending.get() { + return Err(JsValue::from_str("sendPreparedChunk is already in flight")); + } + self.sending.set(true); + let _in_flight = InFlightGuard(&self.sending); + + let spans_vec = self + .prepared_spans + .borrow_mut() + .take() + .ok_or_else(|| JsValue::from_str("no prepared chunk to send"))?; + + // SAFETY: WASM is single-threaded and the `sending` guard above + // guarantees no overlapping invocation, so this is the only live + // reference to the exporter for the duration of the awaits. + let exporter_slot = unsafe { &mut *self.exporter.get() }; + if exporter_slot.is_none() { + // First send: build the exporter asynchronously. `build` is not + // available on wasm (it needs a blocking runtime), so we drive + // `build_async` here where we already have an async context. + let builder = unsafe { &mut *self.builder.get() } + .take() + .ok_or_else(|| JsValue::from_str("exporter builder already consumed"))?; + let built = builder + .build_async::() + .await + .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; + *exporter_slot = Some(built); + } + let exporter = exporter_slot.as_mut().unwrap(); + let resp = exporter + .send_trace_chunks_async(vec![spans_vec]) + .await; + let response_str = resp.map(|resp| match resp { + AgentResponse::Unchanged => "unchanged".to_string(), + AgentResponse::Changed { body } => body, + }); + + response_str + .map(|s| JsValue::from_str(&s)) + .map_err(|e| JsValue::from_str(&format!("{:?}", e))) + } + + /// Flush aggregated stats to the agent's /v0.6/stats endpoint. + /// + /// Should be called periodically (e.g. every 10s) from JS, and with + /// `force=true` on shutdown. + #[wasm_bindgen(js_name = "flushStats")] + pub async fn flush_stats(&self, force: bool) -> Result { + // Take the collector out of the RefCell so no borrow is held across the + // await (the send is async). This also guards re-entrancy: a second + // flushStats (or stats access from prepareChunk) during the await sees + // `None` and is a no-op instead of a double-borrow panic. + let mut collector = match self.stats_collector.borrow_mut().take() { + Some(collector) => collector, + None => return Ok(false), + }; + let result = collector.flush(force).await; + *self.stats_collector.borrow_mut() = Some(collector); + result.map_err(|e| JsValue::from_str(&e)) + } + + /// Flush the queued change-buffer operations. On success always returns + /// `true` (the bool exists only for signature symmetry with the other + /// flush methods); failures surface as a thrown error. + #[wasm_bindgen(js_name = "flushChangeQueue")] + pub fn flush_change_queue(&self) -> Result { + self.cbs.borrow_mut() + .flush_change_buffer() + .map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(true) + } + + /// Set default meta tags applied to every new span. + /// Takes a flat array of key-value pairs: [key1, val1, key2, val2, ...] + #[wasm_bindgen(js_name = "setDefaultMeta")] + pub fn set_default_meta(&self, pairs: Vec) -> Result<(), JsValue> { + let mut tags = Vec::with_capacity(pairs.len() / 2); + let mut i = 0; + while i + 1 < pairs.len() { + let key = pairs[i] + .as_string() + .ok_or_else(|| JsValue::from_str("default meta key must be a string"))?; + let val = pairs[i + 1] + .as_string() + .ok_or_else(|| JsValue::from_str("default meta value must be a string"))?; + tags.push((key.into(), val.into())); + i += 2; + } + self.cbs.borrow_mut().set_default_meta(tags); + Ok(()) + } + + #[wasm_bindgen(js_name = "stringTableInsertOne")] + pub fn string_table_insert_one(&self, key: u32, val: &str) { + self.cbs.borrow_mut() + .string_table_insert_one(key, val.into()); + } + + #[wasm_bindgen(js_name = "stringTableInsertMany")] + pub fn string_table_insert_many(&self, count: u32) -> Result<(), JsValue> { + let mut index: usize = 0; + let mut remaining = count as usize; + // Hold one mutable borrow for the whole bulk insert rather than + // re-borrowing the RefCell once per string. + let mut cbs = self.cbs.borrow_mut(); + let buf = &self.string_table_input; + while remaining > 0 { + // Bound the read against the untrusted `count`: a count larger than + // the encoded entries must error, not index out of bounds. + if index + 4 > buf.len() { + return Err(JsValue::from_str( + "stringTableInsertMany: count exceeds the entries in the input buffer", + )); + } + let key: u32 = get_num(buf, &mut index); + let str_slice = &buf[index..]; + // Bound the NUL scan to the input slice so a non-terminated string + // can't read past the buffer, and advance past the NUL terminator + // (+ 1) so the next entry parses from the right offset. + let cstr = CStr::from_bytes_until_nul(str_slice) + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + let val = cstr + .to_str() + .map_err(|e| JsValue::from_str(&format!("{}", e)))?; + index += val.len() + 1; + // From<&str> for SpanString is a single Arc allocation — no + // intermediate owned String. + cbs.string_table_insert_one(key, val.into()); + remaining -= 1; + } + Ok(()) + } + + #[wasm_bindgen(js_name = "stringTableEvict")] + pub fn string_table_evict(&self, key: u32) { + self.cbs.borrow_mut().string_table_evict_one(key); + } + + // Absent-entity convention: span-level getters return an error (JS throw) + // for an unknown span_id, while trace-level getters and attribute lookups + // return null for an unknown segment / unset attribute. + #[wasm_bindgen(js_name = "getServiceName")] + pub fn get_service_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.service.to_string()) + } + + #[wasm_bindgen(js_name = "getResourceName")] + pub fn get_resource_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.resource.to_string()) + } + + #[wasm_bindgen(js_name = "getMetaAttr")] + pub fn get_meta_attr(&self, span_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + // VecMap::get accepts &str directly (SpanString: Borrow), so no + // SpanString allocation is needed for the lookup. + Ok(span.meta.get(name) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getMetricAttr")] + pub fn get_metric_attr(&self, span_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.metrics.get(name) + .map(|v| JsValue::from_f64(*v)) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getError")] + pub fn get_error(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.error) + } + + // start/duration are i64 nanoseconds. Returning them as JS BigInt (not + // f64) preserves full precision — real epoch-ns values exceed 2^53 and + // would be silently truncated as f64. + #[wasm_bindgen(js_name = "getStart")] + pub fn get_start(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.start) + } + + #[wasm_bindgen(js_name = "getDuration")] + pub fn get_duration(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.duration) + } + + #[wasm_bindgen(js_name = "getType")] + pub fn get_type(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.r#type.to_string()) + } + + #[wasm_bindgen(js_name = "getName")] + pub fn get_name(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + Ok(span.name.to_string()) + } + + // Trace-level attributes live on the Segment (keyed by segment_id, which + // JS allocates and shares across spans in the same local trace). + #[wasm_bindgen(js_name = "getTraceMetaAttr")] + pub fn get_trace_meta_attr(&self, segment_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.meta.get(name)) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getTraceMetricAttr")] + pub fn get_trace_metric_attr(&self, segment_id: u64, name: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.metrics.get(name)) + .map(|v| JsValue::from_f64(*v)) + .unwrap_or(JsValue::NULL)) + } + + #[wasm_bindgen(js_name = "getTraceOrigin")] + pub fn get_trace_origin(&self, segment_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + Ok(cbs.get_segment(&segment_id) + .and_then(|s| s.origin.as_ref()) + .map(|v| JsValue::from_str(v.0.as_ref())) + .unwrap_or(JsValue::NULL)) + } +} + +/// Export WASM memory so JS can create views into it +#[wasm_bindgen(js_name = "getWasmMemory")] +pub fn get_wasm_memory() -> JsValue { + wasm_bindgen::memory() +} + +/// Export OpCode values as a JS object. +/// Values match the `#[repr(u64)]` OpCode enum in libdd-trace-utils. +#[wasm_bindgen(js_name = "getOpCodes")] +pub fn get_op_codes() -> JsValue { + let obj = js_sys::Object::new(); + let entries: &[(&str, u32)] = &[ + ("Create", 0), + ("SetMetaAttr", 1), + ("SetMetricAttr", 2), + ("SetServiceName", 3), + ("SetResourceName", 4), + ("SetError", 5), + ("SetStart", 6), + ("SetDuration", 7), + ("SetType", 8), + ("SetName", 9), + ("SetTraceMetaAttr", 10), + ("SetTraceMetricsAttr", 11), + ("SetTraceOrigin", 12), + ]; + for (name, val) in entries { + js_sys::Reflect::set(&obj, &JsValue::from_str(name), &JsValue::from_f64(*val as f64)) + .unwrap(); + } + obj.into() +} + +#[wasm_bindgen(js_name = "setStorage")] +pub fn set_storage(new_storage: &JsValue) { + libdatadog_nodejs_capabilities::http::set_storage(new_storage); +} + +#[wasm_bindgen(js_name = "setResponseHeaderObserver")] +pub fn set_response_header_observer(observer: &JsValue) { + libdatadog_nodejs_capabilities::http::set_response_header_observer(observer); +} diff --git a/crates/pipeline/src/span_bytes.rs b/crates/pipeline/src/span_bytes.rs new file mode 100644 index 0000000..3dfaf1a --- /dev/null +++ b/crates/pipeline/src/span_bytes.rs @@ -0,0 +1,25 @@ +use libdd_trace_utils::span::SpanBytes; +use serde::Serialize; +use std::borrow::Borrow; +use std::hash::{Hash, Hasher}; + +#[derive(Default, Debug, Eq, PartialEq, Clone, Serialize)] +pub struct SpanBytesImpl(pub Vec); + +impl Hash for SpanBytesImpl { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Borrow<[u8]> for SpanBytesImpl { + fn borrow(&self) -> &[u8] { + &self.0 + } +} + +impl SpanBytes for SpanBytesImpl { + fn from_static_bytes(value: &'static [u8]) -> Self { + SpanBytesImpl(value.to_vec()) + } +} diff --git a/crates/pipeline/src/span_string.rs b/crates/pipeline/src/span_string.rs new file mode 100644 index 0000000..ee75280 --- /dev/null +++ b/crates/pipeline/src/span_string.rs @@ -0,0 +1,46 @@ +use libdd_trace_utils::span::SpanText; +use serde::Serialize; +use std::borrow::Borrow; +use std::fmt::*; +use std::hash::{Hash, Hasher}; +use std::sync::Arc; + +#[derive(Default, Debug, Eq, PartialEq, Serialize, Clone)] +pub struct SpanString(pub Arc); + +impl Hash for SpanString { + fn hash(&self, state: &mut H) { + // Hash the string content, not the Arc pointer + self.0.as_ref().hash(state); + } +} + +impl Borrow for SpanString { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl SpanText for SpanString { + fn from_static_str(value: &'static str) -> Self { + SpanString(Arc::from(value)) + } +} + +impl From for SpanString { + fn from(s: String) -> SpanString { + SpanString(Arc::from(s)) + } +} + +impl From<&str> for SpanString { + fn from(value: &str) -> SpanString { + SpanString(Arc::from(value)) + } +} + +impl Display for SpanString { + fn fmt(&self, formatter: &mut Formatter) -> Result { + write!(formatter, "{}", self.0) + } +} diff --git a/crates/pipeline/src/stats.rs b/crates/pipeline/src/stats.rs new file mode 100644 index 0000000..e533a2b --- /dev/null +++ b/crates/pipeline/src/stats.rs @@ -0,0 +1,149 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Native stats collection for the pipeline WASM module. +//! +//! Wraps `SpanConcentrator` from `libdd-trace-stats` and provides encoding + +//! HTTP transport for flushing stats to the Datadog agent's `/v0.6/stats` +//! endpoint. + +use std::time::{Duration, SystemTime}; + +/// Wall-clock now() for wasm. `std::time::SystemTime::now()` is unimplemented on +/// `wasm32-unknown-unknown` (it panics/traps), so derive the time from JS +/// `Date.now()` (milliseconds since the Unix epoch). +fn now() -> SystemTime { + SystemTime::UNIX_EPOCH + Duration::from_millis(js_sys::Date::now() as u64) +} + +use bytes::Bytes; +use libdd_capabilities::http::HttpClientCapability; +use libdd_trace_protobuf::pb; +use libdd_trace_stats::span_concentrator::SpanConcentrator; +use libdatadog_nodejs_capabilities::DefaultHttpClient; + +use crate::trace_data::WasmTraceData; + +const STATS_ENDPOINT_PATH: &str = "/v0.6/stats"; + +/// Metadata for the stats payload envelope. +pub struct StatsMeta { + pub hostname: String, + pub env: String, + pub version: String, + pub lang: String, + pub tracer_version: String, + pub runtime_id: String, + pub service: String, +} + +/// Manages stats aggregation and flushing. +pub struct StatsCollector { + concentrator: SpanConcentrator, + meta: StatsMeta, + agent_url: String, + sequence: u64, +} + +impl StatsCollector { + /// Create a new stats collector. + pub fn new(bucket_size: Duration, agent_url: String, meta: StatsMeta) -> Self { + StatsCollector { + concentrator: SpanConcentrator::new( + bucket_size, + now(), + vec![ + "client".to_string(), + "server".to_string(), + "producer".to_string(), + "consumer".to_string(), + ], + Vec::new(), + ), + meta, + agent_url, + sequence: 0, + } + } + + /// Add spans to the concentrator for stats aggregation. + /// + /// The spans should already have `_dd.top_level` and `_dd.measured` metrics + /// set (done by `ChangeBufferState::flush_chunk`). + pub fn add_spans(&mut self, spans: &[libdd_trace_utils::span::v04::Span]) { + for span in spans { + self.concentrator.add_span(span); + } + } + + /// Flush aggregated stats and send to the agent. + /// + /// Returns `Ok(true)` if stats were sent, `Ok(false)` if there was nothing + /// to send, or `Err` on transport failure. + pub async fn flush(&mut self, force: bool) -> Result { + let buckets = self.concentrator.flush(now(), force); + if buckets.is_empty() { + return Ok(false); + } + + self.sequence += 1; + let payload = encode_stats_payload(&buckets, &self.meta, self.sequence); + + let body = rmp_serde::encode::to_vec_named(&payload) + .map_err(|e| format!("stats msgpack encode error: {e}"))?; + + let stats_url = format!("{}{}", self.agent_url, STATS_ENDPOINT_PATH); + let uri: http::Uri = stats_url + .parse() + .map_err(|e| format!("invalid stats URL: {e}"))?; + + let req = http::Request::builder() + .method(http::Method::PUT) + .uri(uri) + .header("Content-Type", "application/msgpack") + .header("Datadog-Meta-Lang", &self.meta.lang) + .header("Datadog-Meta-Tracer-Version", &self.meta.tracer_version) + .body(Bytes::from(body)) + .map_err(|e| format!("failed to build stats request: {e}"))?; + + let client = DefaultHttpClient::new_client(); + client + .request(req) + .await + .map_err(|e| format!("stats send error: {e:?}"))?; + + Ok(true) + } + + /// Update the agent URL (e.g. after reconfiguration). + pub fn set_agent_url(&mut self, url: String) { + self.agent_url = url; + } +} + +/// Encode flushed stats buckets into a `ClientStatsPayload` for msgpack +/// serialization. +fn encode_stats_payload( + buckets: &[pb::ClientStatsBucket], + meta: &StatsMeta, + sequence: u64, +) -> pb::ClientStatsPayload { + pb::ClientStatsPayload { + hostname: meta.hostname.clone(), + env: meta.env.clone(), + version: meta.version.clone(), + lang: meta.lang.clone(), + tracer_version: meta.tracer_version.clone(), + runtime_id: meta.runtime_id.clone(), + sequence, + stats: buckets.to_vec(), + service: meta.service.clone(), + container_id: String::new(), + tags: Vec::new(), + agent_aggregation: String::new(), + git_commit_sha: String::new(), + image_tag: String::new(), + process_tags: String::new(), + process_tags_hash: 0, + } +} diff --git a/crates/pipeline/src/trace_data.rs b/crates/pipeline/src/trace_data.rs new file mode 100644 index 0000000..2546477 --- /dev/null +++ b/crates/pipeline/src/trace_data.rs @@ -0,0 +1,12 @@ +use libdd_trace_utils::span::TraceData; + +use crate::span_bytes::SpanBytesImpl; +use crate::span_string::SpanString; + +#[derive(Clone, Default, Debug, PartialEq)] +pub struct WasmTraceData; + +impl TraceData for WasmTraceData { + type Text = SpanString; + type Bytes = SpanBytesImpl; +} diff --git a/crates/pipeline/src/utils.rs b/crates/pipeline/src/utils.rs new file mode 100644 index 0000000..aafbfa5 --- /dev/null +++ b/crates/pipeline/src/utils.rs @@ -0,0 +1,37 @@ +pub trait FromBytes: Sized { + type Bytes: ?Sized; + fn from_bytes(bytes: &[u8]) -> Self; +} + +macro_rules! impl_from_bytes { + ($ty:ty, $len:expr) => { + impl FromBytes for $ty { + type Bytes = $ty; + + // Note that this always does a copy into a new variable. This is + // because the values in the buffer are not aligned. We could save + // ourselves a copy by ensuring alignment from the managed side. + fn from_bytes(bytes: &[u8]) -> Self { + let mut code_buf = [0u8; $len]; + code_buf.copy_from_slice(bytes); + <$ty>::from_le_bytes(code_buf) + } + } + }; +} + +impl_from_bytes!(u128, 16); +impl_from_bytes!(u64, 8); +impl_from_bytes!(f64, 8); +impl_from_bytes!(i64, 8); +impl_from_bytes!(i32, 4); +impl_from_bytes!(u32, 4); + +pub fn get_num(buf: &[u8], index: &mut usize) -> T { + let id: usize = *index; + let size = std::mem::size_of::(); + let result = &buf[id..(id + size)]; + let result: T = T::from_bytes(result); + *index += size; + result +} diff --git a/test/pipeline.js b/test/pipeline.js index 239b3de..e627ffd 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -1,10 +1,701 @@ 'use strict' +const { describe, it, before, beforeEach } = require('node:test') +const assert = require('node:assert') +const crypto = require('crypto') + const pipeline = require('..').maybeLoad('pipeline') +const { WasmSpanState } = pipeline +const OpCode = pipeline.getOpCodes() +const wasmMemory = pipeline.getWasmMemory() + +function getRandomBytes (byteCount) { + return new Uint8Array(crypto.randomBytes(byteCount)) +} + +function bytesToBigInt (bytes) { + let val = 0n + for (let i = 0; i < bytes.length; i++) { + val = (val << 8n) | BigInt(bytes[i]) + } + return val +} + +// The Span and NativeSpansInterface classes act as a sketch of what should +// be implemented in dd-trace-js. + +// TODO should NativeSpansInterface actually be implemented in this package? + +class Span { + constructor (nativeSpans, traceId, parentId) { + this.nativeSpans = nativeSpans + this.traceId = traceId || [getRandomBytes(8), getRandomBytes(8)] + this.parentId = parentId || new Uint8Array(8) + this.spanId = getRandomBytes(8) + // Spans are addressed by their span_id (u64). Operations carry the raw + // 8-byte id in their header; getters take the numeric id as a BigInt. + this.spanIdBig = bytesToBigInt(this.spanId) + // Trace-level attributes live on a Segment (a local trace chunk). JS owns + // segment_id allocation and shares it across spans in the same trace. + this.segmentId = nativeSpans.allocSegment(this.traceId) + this._startTime = BigInt(Date.now()) * 1000000n + + this.nativeSpans.queueOp(OpCode.Create, this.spanId, ['u128', this.traceId], ['u64n', this.segmentId], ['u64', this.parentId]) + this.nativeSpans.queueOp(OpCode.SetStart, this.spanId, ['i64', this._startTime]) + } + + setTag (key, value) { + if (typeof value === 'number') { + this.nativeSpans.queueOp(OpCode.SetMetricAttr, this.spanId, key, ['f64', value]) + } else { + this.nativeSpans.queueOp(OpCode.SetMetaAttr, this.spanId, key, value) + } + return this + } + + getTag (key) { + return this.nativeSpans.state.getMetaAttr(this.spanIdBig, key) ?? + this.nativeSpans.state.getMetricAttr(this.spanIdBig, key) + } + + setTraceTag (key, value) { + const opcode = OpCode[typeof value === 'number' ? 'SetTraceMetricsAttr' : 'SetTraceMetaAttr'] + if (typeof value === 'number') { + value = ['f64', value] + } + this.nativeSpans.queueOp(opcode, this.spanId, key, value) + return this + } + + getTraceTag (key) { + return this.nativeSpans.state.getTraceMetaAttr(this.segmentId, key) ?? + this.nativeSpans.state.getTraceMetricAttr(this.segmentId, key) + } + + setTraceOrigin (origin) { + this.nativeSpans.queueOp(OpCode.SetTraceOrigin, this.spanId, origin) + return this + } + + getTraceOrigin () { + return this.nativeSpans.state.getTraceOrigin(this.segmentId) + } + + finish () { + this.duration = BigInt(Date.now()) * 1000000n - this._startTime + return this + } +} + +const spanAccessors = { + // [getterName, opCode, valueType (null for string)] + name: ['getName', 'SetName', null], + service: ['getServiceName', 'SetServiceName', null], + resource: ['getResourceName', 'SetResourceName', null], + type: ['getType', 'SetType', null], + error: ['getError', 'SetError', 'i32'], + start: ['getStart', null, null], + duration: ['getDuration', 'SetDuration', 'i64'] +} + +Object.entries(spanAccessors).forEach(([prop, [getter, setter, valueType]]) => { + Object.defineProperty(Span.prototype, prop, { + get () { + return this.nativeSpans.state[getter](this.spanIdBig) + }, + set (val) { + val = valueType ? [valueType, val] : val + this.nativeSpans.queueOp(OpCode[setter], this.spanId, val); + } + }) +}) + +const CHANGE_QUEUE_SIZE = 64 * 1024 +const STRING_TABLE_INPUT_SIZE = 10 * 1024 + +class NativeSpansInterface { + constructor (options = {}) { + this.flushBuffer = Buffer.alloc(10 * 1024) + + this.cqbIndex = 8 // Start at 8 since first u64 is count + this.cqbCount = 0 + this.stibCount = 0 + this.segmentCount = 0 // Monotonic segment_id allocator + this.segmentByTrace = new Map() // trace key -> segment_id (BigInt) + this.stringMap = new Map() -if (pipeline) { - pipeline.init_trace_exporter('127.0.0.1', 8126, 10_000, '1.0', 'nodejs', '18.0', 'v8') + this.state = new WasmSpanState( + options.agentUrl || process.env.AGENT_URL || 'http://127.0.0.1:8126', + options.tracerVersion || '1.0.0', + options.lang || 'nodejs', + options.langVersion || process.version, + options.langInterpreter || 'v8', + CHANGE_QUEUE_SIZE, + STRING_TABLE_INPUT_SIZE, + options.pid ?? process.pid, + options.tracerService || 'test-service', + options.statsEnabled ?? false, + options.hostname || 'test-host', + options.env || 'test-env', + options.appVersion || '1.0.0', + options.runtimeId || '00000000-0000-0000-0000-000000000000' + ) - const ret = pipeline.send_traces(Buffer.alloc(1), 1) - console.log(ret) + // Get pointers into WASM memory for direct buffer access + this._wasmMemory = wasmMemory + this._cqbPtr = this.state.change_queue_ptr() + this._refreshViews() + } + + _refreshViews () { + this._cqbView = new DataView(this._wasmMemory.buffer, this._cqbPtr) + this._cqbBytes = new Uint8Array(this._wasmMemory.buffer, this._cqbPtr) + } + + resetChangeQueue () { + this.cqbIndex = 8 + this.cqbCount = 0 + // Check if WASM memory was detached/grown + if (this._wasmMemory.buffer !== this._cqbView.buffer) { + this._refreshViews() + } + this._cqbView.setUint32(0, 0, true) + this._cqbView.setUint32(4, 0, true) + } + + flushChangeQueue () { + this.state.flushChangeQueue() + this.resetChangeQueue() + } + + getStringId (str) { + let id = this.stringMap.get(str) + if (typeof id === 'number') return id + + id = this.stibCount++ + this.stringMap.set(str, id) + this.state.stringTableInsertOne(id, str) + return id + } + + // Write 8 big-endian bytes as a little-endian u64 into the change buffer + _writeBytesLE (bytes, offset) { + const buf = this._cqbBytes + for (let i = 0; i < 8; i++) { + buf[offset + i] = bytes[7 - i] + } + } + + // Allocate (or reuse) a segment_id for a given trace. Spans sharing a trace + // share a segment so trace-level attributes are visible across them. + allocSegment (traceId) { + const key = traceId.map(b => Buffer.from(b).toString('hex')).join('') + let id = this.segmentByTrace.get(key) + if (id === undefined) { + id = BigInt(this.segmentCount++) + this.segmentByTrace.set(key, id) + } + return id + } + + queueOp (op, spanId, ...args) { + // Check if WASM memory was detached/grown + if (this._wasmMemory.buffer !== this._cqbView.buffer) { + this._refreshViews() + } + + // Check if Rust flushed the queue (wrote 0 to count position) + if (this._cqbView.getUint32(0, true) === 0 && this.cqbCount > 0) { + this.cqbIndex = 8 + this.cqbCount = 0 + } + + const view = this._cqbView + // Op header: opcode (u16 LE) + span_id (u64 LE) = 10 bytes. Rust reads the + // opcode as a u16, then the span_id as a u64. + view.setUint16(this.cqbIndex, op, true) + this.cqbIndex += 2 + this._writeBytesLE(spanId, this.cqbIndex) + this.cqbIndex += 8 + + for (const arg of args) { + if (typeof arg === 'string') { + const stringId = this.getStringId(arg) + view.setUint32(this.cqbIndex, stringId, true) + this.cqbIndex += 4 + } else { + const [typ, num] = arg + switch (typ) { + case 'u64': + this._writeBytesLE(num, this.cqbIndex) + this.cqbIndex += 8 + break + case 'u64n': + view.setBigUint64(this.cqbIndex, BigInt(num), true) + this.cqbIndex += 8 + break + case 'u32n': // raw, pre-resolved string-table id + view.setUint32(this.cqbIndex, num, true) + this.cqbIndex += 4 + break + case 'u128': + this._writeBytesLE(num[0], this.cqbIndex) + this.cqbIndex += 8 + this._writeBytesLE(num[1], this.cqbIndex) + this.cqbIndex += 8 + break + case 'i64': + view.setBigInt64(this.cqbIndex, num, true) + this.cqbIndex += 8 + break + case 'i32': + view.setInt32(this.cqbIndex, num, true) + this.cqbIndex += 4 + break + case 'f64': + view.setFloat64(this.cqbIndex, num, true) + this.cqbIndex += 8 + break + default: + throw new Error('unsupported number type: ' + typ) + } + } + } + + this.cqbCount++ + view.setBigUint64(0, BigInt(this.cqbCount), true) + } + + createSpan (traceId, parentId) { + return new Span(this, traceId, parentId) + } + + async flushSpans (...spans) { + this.flushBuffer.fill(0) // TODO is this necessary, since we're sending the length? + let index = 0 + for (const span of spans) { + // The chunk buffer carries u64 span IDs (8 bytes LE each). + const spanId = span.spanId ?? span + for (let i = 0; i < 8; i++) { + this.flushBuffer[index + i] = spanId[7 - i] + } + index += 8 + } + const hasSpans = this.state.prepareChunk(spans.length, true, this.flushBuffer) + if (!hasSpans) return false + return this.state.sendPreparedChunk() + } } + +describe('pipeline', () => { + let nativeSpans + + before(() => { + nativeSpans = new NativeSpansInterface() + }) + + beforeEach(() => { + nativeSpans.resetChangeQueue() + }) + + describe('module exports', () => { + it('should export WasmSpanState', () => { + assert(WasmSpanState) + }) + + it('should export OpCode', () => { + assert(OpCode) + }) + + it('should export all OpCodes', () => { + const expectedOpCodes = [ + 'Create', 'SetMetaAttr', 'SetMetricAttr', 'SetServiceName', + 'SetResourceName', 'SetError', 'SetStart', 'SetDuration', + 'SetType', 'SetName', 'SetTraceMetaAttr', 'SetTraceMetricsAttr', + 'SetTraceOrigin' + ] + for (const opCode of expectedOpCodes) { + assert.strictEqual(typeof OpCode[opCode], 'number') + } + }) + }) + + describe('WasmSpanState', () => { + it('should create an instance', () => { + assert(nativeSpans.state instanceof WasmSpanState) + }) + }) + + describe('span creation', () => { + it('should create a span with basic attributes', () => { + const span = nativeSpans.createSpan() + span.name = 'test-span' + span.service = 'test-service' + span.resource = '/api/test' + span.type = 'web' + span.error = 0 + + assert.strictEqual(span.name, 'test-span') + assert.strictEqual(span.service, 'test-service') + assert.strictEqual(span.resource, '/api/test') + assert.strictEqual(span.type, 'web') + assert.strictEqual(span.error, 0) + }) + + it('should create a child span with parent', () => { + const parent = nativeSpans.createSpan() + parent.name = 'parent-span' + + const child = nativeSpans.createSpan(parent.traceId, parent.spanId) + child.name = 'child-span' + + assert.strictEqual(child.name, 'child-span') + }) + }) + + describe('span attributes', () => { + it('should set and get string tags', () => { + const span = nativeSpans.createSpan() + span.setTag('http.method', 'GET') + span.setTag('http.url', 'http://example.com/api') + + assert.strictEqual(span.getTag('http.method'), 'GET') + assert.strictEqual(span.getTag('http.url'), 'http://example.com/api') + }) + + it('should set and get numeric tags', () => { + const span = nativeSpans.createSpan() + span.setTag('http.status_code', 200) + span.setTag('custom.metric', 3.14159) + + assert.strictEqual(span.getTag('http.status_code'), 200) + assert.strictEqual(span.getTag('custom.metric'), 3.14159) + }) + + it('should set and get error state', () => { + const span = nativeSpans.createSpan() + span.error = 0 + assert.strictEqual(span.error, 0) + + span.error = 1 + assert.strictEqual(span.error, 1) + }) + }) + + describe('span timing', () => { + it('should set and get start time', () => { + const span = nativeSpans.createSpan() + assert(span.start > 0n) // populated from the constructor's SetStart (BigInt ns) + + // Verify an exact round-trip. getStart returns an f64, so real ns + // timestamps (> 2^53) can't be checked to the nanosecond; use a small, + // exactly-representable value so an off-by-one would actually be caught. + nativeSpans.queueOp(OpCode.SetStart, span.spanId, ['i64', 12_345n]) + assert.strictEqual(span.start, 12_345n) + }) + + it('should set and get duration', () => { + const duration = 1000000n + const span = nativeSpans.createSpan() + span.duration = duration + // getDuration returns i64 nanoseconds as a BigInt (no f64 truncation). + assert.strictEqual(span.duration, duration) + }) + }) + + describe('trace-level attributes', () => { + it('should set and get trace string tags', () => { + const span = nativeSpans.createSpan() + span.setTraceTag('_dd.p.dm', '-0') + assert.strictEqual(span.getTraceTag('_dd.p.dm'), '-0') + }) + + it('should set and get trace numeric tags', () => { + const span = nativeSpans.createSpan() + span.setTraceTag('_sampling_priority_v1', 1) + assert.strictEqual(span.getTraceTag('_sampling_priority_v1'), 1) + }) + + it('should set and get trace origin', () => { + const span = nativeSpans.createSpan() + span.setTraceOrigin('synthetics') + assert.strictEqual(span.getTraceOrigin(), 'synthetics') + }) + + it('should share trace attributes across spans in same trace', () => { + const parent = nativeSpans.createSpan() + parent.setTraceTag('shared_key', 'shared_value') + parent.setTraceTag('shared_metric', 42) + parent.setTraceOrigin('lambda') + + const child = nativeSpans.createSpan(parent.traceId, parent.spanId) + + assert.strictEqual(child.getTraceTag('shared_key'), 'shared_value') + assert.strictEqual(child.getTraceTag('shared_metric'), 42) + assert.strictEqual(child.getTraceOrigin(), 'lambda') + }) + + it('should isolate trace attributes across different traces', () => { + const a = nativeSpans.createSpan() + a.setTraceTag('iso_key', 'a_value') + a.setTraceOrigin('origin-a') + + // A span in a DIFFERENT trace must not see trace a's segment data. + const b = nativeSpans.createSpan() + assert.notStrictEqual(a.segmentId, b.segmentId) + assert.strictEqual(b.getTraceTag('iso_key'), null) + assert.strictEqual(b.getTraceOrigin(), null) + }) + }) + + describe('absent values and error handling', () => { + it('returns null for tags that were never set', () => { + const span = nativeSpans.createSpan() + assert.strictEqual(nativeSpans.state.getMetaAttr(span.spanIdBig, 'never-set'), null) + assert.strictEqual(nativeSpans.state.getMetricAttr(span.spanIdBig, 'never-set'), null) + assert.strictEqual(span.getTag('never-set'), null) + }) + + it('throws when reading an unknown span id', () => { + // Convention: span-level getters throw on an unknown span_id, while + // trace-level getters return null for an unknown segment. All span + // getters share the get_span error path, so assert each one throws. + const bogus = 0xdeadbeefn + for (const getter of [ + 'getName', 'getServiceName', 'getResourceName', + 'getType', 'getError', 'getStart', 'getDuration' + ]) { + assert.throws(() => nativeSpans.state[getter](bogus), `${getter} should throw`) + } + }) + }) + + describe('default meta', () => { + it('applies default meta to new spans and validates inputs', () => { + // Fresh interface so the default doesn't leak into the shared instance. + const ns = new NativeSpansInterface() + ns.state.setDefaultMeta(['dk', 'dv']) + const span = ns.createSpan() + assert.strictEqual(span.getTag('dk'), 'dv') + + // Non-string key or value must throw. + assert.throws(() => ns.state.setDefaultMeta(['k', 123])) + assert.throws(() => ns.state.setDefaultMeta([123, 'v'])) + // A trailing unpaired key is ignored, not an error. + assert.doesNotThrow(() => ns.state.setDefaultMeta(['lonely'])) + }) + }) + + describe('sampling', () => { + it('should not expose sample() in WASM module', () => { + // Sampling is handled JS-side; the WASM module does not expose a sample() method + assert.strictEqual(typeof nativeSpans.state.sample, 'undefined') + }) + }) + + describe('string table', () => { + it('should evict strings from the table', () => { + const testKey = 'eviction-test-key-' + Math.random() + const testVal = 'eviction-test-value' + const span = nativeSpans.createSpan() + span.setTag(testKey, testVal) + + assert.strictEqual(span.getTag(testKey), testVal) + + const keyId = nativeSpans.stringMap.get(testKey) + assert.strictEqual(typeof keyId, 'number', 'key was interned in the string table') + nativeSpans.state.stringTableEvict(keyId) + + // Evicting the key from the string table must not affect spans that have + // already resolved it: the tag was materialized onto the span at flush + // time, so the span keeps its own copy of the value. + assert.strictEqual(span.getTag(testKey), testVal) + }) + + it('bulk-inserts strings via stringTableInsertMany', () => { + // Wire format per entry: [key:u32 LE][cstr bytes][NUL]. Two entries + // exercise the NUL-terminator advance (a missing +1 would misparse the + // second entry). + const ptr = nativeSpans.state.string_table_input_ptr() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr) + const entries = [[60001, 'bulk-key'], [60002, 'bulk-val']] + let off = 0 + for (const [key, str] of entries) { + view.setUint32(off, key, true); off += 4 + for (let i = 0; i < str.length; i++) bytes[off++] = str.charCodeAt(i) + bytes[off++] = 0 + } + nativeSpans.state.stringTableInsertMany(entries.length) + + // Reference the pre-inserted ids directly (raw u32, not via getStringId) + // in a SetMetaAttr op; at flush they must resolve to the bulk strings. + const span = nativeSpans.createSpan() + nativeSpans.queueOp(OpCode.SetMetaAttr, span.spanId, ['u32n', 60001], ['u32n', 60002]) + assert.strictEqual(span.getTag('bulk-key'), 'bulk-val') + }) + + it('rejects a malformed (non-terminated) stringTableInsertMany entry', () => { + const ptr = nativeSpans.state.string_table_input_ptr() + const len = nativeSpans.state.string_table_input_len() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr, len) + bytes.fill(0xff) // no NUL terminator anywhere in the buffer + view.setUint32(0, 70001, true) + // from_bytes_until_nul finds no terminator -> error surfaced as a throw, + // not an out-of-bounds read. + assert.throws(() => nativeSpans.state.stringTableInsertMany(1)) + }) + + it('rejects a stringTableInsertMany count larger than the buffer holds', () => { + const ptr = nativeSpans.state.string_table_input_ptr() + const len = nativeSpans.state.string_table_input_len() + const view = new DataView(wasmMemory.buffer, ptr) + const bytes = new Uint8Array(wasmMemory.buffer, ptr, len) + bytes.fill(0) + // One valid entry consuming almost the whole buffer, so claiming a count + // of 2 makes the second u32 key read run past the end -> bounded error, + // not an out-of-bounds panic. + view.setUint32(0, 71000, true) + for (let i = 4; i < len - 1; i++) bytes[i] = 0x61 // 'a' + bytes[len - 1] = 0 // NUL terminator at the very end + assert.throws(() => nativeSpans.state.stringTableInsertMany(2), /exceeds the entries/) + }) + }) + + describe('input validation', () => { + it('throws when prepareChunk len exceeds the chunk size', () => { + // 100 span ids would need 800 bytes; the chunk only has 8. + assert.throws(() => nativeSpans.state.prepareChunk(100, true, Buffer.alloc(8))) + }) + + it('flushSpans with no spans is a no-op returning false', async () => { + assert.strictEqual(await nativeSpans.flushSpans(), false) + }) + }) + + describe('flush to agent', () => { + it('should flush spans to a (mock) agent', async () => { + // Stand up a throwaway HTTP server acting as the agent so the flush path + // (prepareChunk -> build exporter -> serialize -> send) is exercised + // end-to-end in CI, instead of being skipped when no agent is present. + const http = require('node:http') + const payloads = [] + const server = http.createServer((req, res) => { + const chunks = [] + req.on('data', c => chunks.push(c)) + req.on('end', () => { + payloads.push(Buffer.concat(chunks)) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + const span = ns.createSpan() + span.name = 'flush-test-span' + span.service = 'test-service' + span.resource = 'test-resource' + span.type = 'web' + span.duration = 1000000n + + try { + const result = await ns.flushSpans(span) + assert(result, 'exporter returned an agent response') + assert(payloads.length > 0, 'agent received a trace payload') + assert(payloads[0].length > 0, 'trace payload is non-empty') + } finally { + server.closeAllConnections?.() + server.close() + } + }) + }) + + describe('client-side stats', () => { + it('aggregates and flushes stats to /v0.6/stats', async () => { + const http = require('node:http') + const seen = [] + const server = http.createServer((req, res) => { + const chunks = [] + req.on('data', c => chunks.push(c)) + req.on('end', () => { + seen.push({ method: req.method, url: req.url, len: Buffer.concat(chunks).length }) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + + // statsEnabled:true builds the StatsCollector; prepareChunk feeds spans + // into it, and flushStats(true) force-flushes to /v0.6/stats. + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}`, statsEnabled: true }) + const span = ns.createSpan() + span.name = 'stats-span' + span.service = 'stats-svc' + span.resource = '/stats' + span.type = 'web' + span.duration = 5_000_000n + + try { + await ns.flushSpans(span) + const sent = await ns.state.flushStats(true) + assert.strictEqual(sent, true, 'flushStats reported a send') + const statsReq = seen.find(r => r.url === '/v0.6/stats') + assert.ok(statsReq, 'agent received a /v0.6/stats request') + assert.strictEqual(statsReq.method, 'PUT') + assert.ok(statsReq.len > 0, 'stats payload is non-empty') + + // Nothing new aggregated -> a second forced flush is a no-op. + assert.strictEqual(await ns.state.flushStats(true), false, 'second flush has nothing to send') + } finally { + server.closeAllConnections?.() + server.close() + } + }) + + it('flushStats returns false when stats are disabled', async () => { + const ns = new NativeSpansInterface({ statsEnabled: false }) + assert.strictEqual(await ns.state.flushStats(true), false) + }) + }) + + describe('send re-entrancy', () => { + it('rejects an overlapping sendPreparedChunk call', async () => { + const http = require('node:http') + const server = http.createServer((req, res) => { + req.resume() + req.on('end', () => { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{}') }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + const span = ns.createSpan() + span.name = 'reentrancy' + ns.flushBuffer.fill(0) + for (let i = 0; i < 8; i++) ns.flushBuffer[i] = span.spanId[7 - i] + assert.ok(ns.state.prepareChunk(1, true, ns.flushBuffer)) + + try { + // Two calls without awaiting the first: the in-flight guard must reject + // the second instead of aliasing the exporter (UB). + const settled = await Promise.allSettled([ + ns.state.sendPreparedChunk(), + ns.state.sendPreparedChunk() + ]) + const reasons = settled + .filter(s => s.status === 'rejected') + .map(s => String(s.reason)) + assert.ok( + reasons.some(r => /already in flight/.test(r)), + 'one overlapping call rejected as already-in-flight' + ) + } finally { + server.closeAllConnections?.() + server.close() + } + }) + }) +}) From 5decee4726bcb7ce49ab95ee24a049d243882deb Mon Sep 17 00:00:00 2001 From: Bryan English Date: Tue, 23 Jun 2026 16:33:46 -0400 Subject: [PATCH 04/12] feat(trace-exporter): add wasm TraceExporter binding Expose libdatadog's TraceExporter to JS via wasm-bindgen, pinned to the wasm capability bundle. The exporter is built lazily on first send, since the blocking build path is unavailable on wasm. Includes an integration test that drives it against an in-process mock agent. Co-authored-by: Jules Wiriath Co-authored-by: paullegranddc Co-authored-by: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> --- crates/trace_exporter/Cargo.toml | 25 +++++++ crates/trace_exporter/src/lib.rs | 114 ++++++++++++++++++++++++++++++ test/wasm/trace_exporter/index.js | 63 +++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 crates/trace_exporter/Cargo.toml create mode 100644 crates/trace_exporter/src/lib.rs create mode 100644 test/wasm/trace_exporter/index.js diff --git a/crates/trace_exporter/Cargo.toml b/crates/trace_exporter/Cargo.toml new file mode 100644 index 0000000..f85b7c3 --- /dev/null +++ b/crates/trace_exporter/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "trace-exporter" +version = "0.1.0" +edition = "2021" +description = "Wasm binding for libdd-data-pipeline TraceExporter" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" +libdatadog-nodejs-capabilities = { path = "../capabilities" } +libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } +libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } +console_error_panic_hook = "0.1" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1", features = ["js"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" diff --git a/crates/trace_exporter/src/lib.rs b/crates/trace_exporter/src/lib.rs new file mode 100644 index 0000000..b5cf23d --- /dev/null +++ b/crates/trace_exporter/src/lib.rs @@ -0,0 +1,114 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Wasm binding for `TraceExporter`. +//! +//! This crate exposes libdatadog's trace exporter to JavaScript via +//! `wasm_bindgen`. The generic parameter is pinned to `WasmCapabilities`, +//! which routes I/O (HTTP, etc.) back into JavaScript. +//! +//! This is a lower-level, secondary path that sends pre-encoded v0.4 trace +//! bytes directly; the primary span pipeline (and its richer tracer metadata) +//! lives in the `pipeline` crate's `WasmSpanState`. `JsTraceExporter` hardcodes +//! its language metadata accordingly. + +use std::cell::{Cell, UnsafeCell}; + +use libdatadog_nodejs_capabilities::WasmCapabilities; +use libdd_data_pipeline::trace_exporter::{ + agent_response::AgentResponse, TraceExporter, TraceExporterBuilder, TraceExporterInputFormat, + TraceExporterOutputFormat, +}; +use libdd_shared_runtime::LocalRuntime; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(start)] +fn init() { + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +pub struct JsTraceExporter { + /// Built lazily on first send: `build` is unavailable on wasm (it needs a + /// blocking runtime), so the configured builder is stashed and driven via + /// `build_async` inside the async send path. + // wasm uses the single-threaded LocalRuntime (spawn_local), not the + // native multi-thread runtimes. + inner: UnsafeCell>>, + builder: UnsafeCell>>, + /// Re-entrancy guard for `send`. wasm-bindgen async exports can be + /// re-invoked from JS before the prior future resolves; without this, two + /// calls would each take `&mut` out of `inner`/`builder` and alias across + /// the await (UB). A re-entrant call returns an error instead. + sending: Cell, +} + +// SAFETY: wasm is single-threaded; there are no concurrent accesses. +unsafe impl Send for JsTraceExporter {} +unsafe impl Sync for JsTraceExporter {} + +/// Clears the in-flight flag on drop (covers early returns / dropped futures). +struct InFlightGuard<'a>(&'a Cell); +impl Drop for InFlightGuard<'_> { + fn drop(&mut self) { + self.0.set(false); + } +} + +#[wasm_bindgen] +impl JsTraceExporter { + #[wasm_bindgen(constructor)] + pub fn new(url: &str, service: &str) -> Result { + let mut builder = TraceExporterBuilder::::new(); + builder + .set_url(url) + .set_service(service) + .set_language("javascript") + .set_language_version("") + .set_language_interpreter("nodejs") + .set_input_format(TraceExporterInputFormat::V04) + .set_output_format(TraceExporterOutputFormat::V04); + + Ok(JsTraceExporter { + inner: UnsafeCell::new(None), + builder: UnsafeCell::new(Some(builder)), + sending: Cell::new(false), + }) + } + + #[wasm_bindgen] + pub async fn send(&self, data: &[u8]) -> Result { + if self.sending.get() { + return Err(JsValue::from_str("send is already in flight")); + } + self.sending.set(true); + let _in_flight = InFlightGuard(&self.sending); + + // SAFETY: wasm is single-threaded and the `sending` guard above rejects + // re-entrant calls, so this is the only live reference across the await. + let slot = unsafe { &mut *self.inner.get() }; + if slot.is_none() { + let builder = unsafe { &mut *self.builder.get() } + .take() + .ok_or_else(|| JsValue::from_str("exporter builder already consumed"))?; + let built = builder + .build_async::() + .await + .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; + *slot = Some(built); + } + let inner = slot.as_ref().unwrap(); + + let result = inner + .send_async(data) + .await + .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; + + // Mirror WasmSpanState.sendPreparedChunk's representation so both send + // paths report an unchanged agent response identically. + match result { + AgentResponse::Changed { body } => Ok(JsValue::from_str(&body)), + AgentResponse::Unchanged => Ok(JsValue::from_str("unchanged")), + } + } +} diff --git a/test/wasm/trace_exporter/index.js b/test/wasm/trace_exporter/index.js new file mode 100644 index 0000000..f4e2b39 --- /dev/null +++ b/test/wasm/trace_exporter/index.js @@ -0,0 +1,63 @@ +'use strict' + +// This test exercises the full call flow: +// JS -> wasm (TraceExporter logic) -> JS (http_transport.js for I/O) -> wasm -> JS +// +// To run: +// 1. Build: wasm-pack build --target nodejs ./crates/trace_exporter --out-dir ../../prebuilds/trace_exporter +// 2. Run: node test_wasm.js trace_exporter + +const http = require('http') +const loader = require('../../../load.js') +const assert = require('assert') + +const traceExporter = loader.load('trace_exporter') +assert(traceExporter !== undefined, 'trace_exporter wasm module loaded') + +// Start a minimal HTTP server that acts as a mock Datadog agent +let receivedBytes = -1 +const server = http.createServer((req, res) => { + let body = [] + req.on('data', chunk => body.push(chunk)) + req.on('end', () => { + receivedBytes = Buffer.concat(body).length + // Return a minimal agent response + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ rate_by_service: {} })) + }) +}) + +server.listen(0, '127.0.0.1', async () => { + const port = server.address().port + const url = `http://127.0.0.1:${port}` + + try { + // Create a TraceExporter pointing at the mock agent + const exporter = new traceExporter.JsTraceExporter(url, 'test-service') + assert(exporter !== undefined, 'JsTraceExporter created') + + // Send a minimal msgpack-encoded v0.4 trace payload (empty array of traces) + // This is msgpack for [[]] — one trace containing no spans + const payload = new Uint8Array([0x91, 0x90]) + const result = await exporter.send(payload) + + // send() resolves to the agent response body (or the sentinel 'unchanged'); + // either way a successful round-trip yields a truthy string, and the mock + // agent must have actually received the payload bytes. + assert.ok(result, 'send() returned a truthy agent response') + assert.strictEqual(typeof result, 'string', 'send() result is a string') + assert.ok(receivedBytes >= 0, 'mock agent received the trace request') + console.log('Trace export result:', result) + console.log('PASS: wasm trace exporter integration test') + } catch (err) { + console.error('Test error:', err) + process.exitCode = 1 + } finally { + server.close() + } + + // The wasm trace exporter keeps the event loop alive after a send (its + // runtime machinery has no shutdown on wasm), so exit explicitly once the + // assertions are done — mirrors --test-force-exit for the node:test files. + process.exit(process.exitCode || 0) +}) From eed4f3693d1a2fc0e5a12670712c18d6d2a56d0c Mon Sep 17 00:00:00 2001 From: Bryan English Date: Tue, 23 Jun 2026 16:33:47 -0400 Subject: [PATCH 05/12] chore: adapt existing crates for toolchain and libdatadog bumps Update library_config, process_discovery, and datadog-js-zstd for the workspace's pinned Rust toolchain and libdatadog dependency versions. Co-authored-by: Jules Wiriath Co-authored-by: paullegranddc Co-authored-by: Gyuheon Oh <102937919+gyuheon0h@users.noreply.github.com> --- crates/datadog-js-zstd/src/lib.rs | 7 ++----- crates/library_config/Cargo.toml | 2 +- crates/library_config/src/lib.rs | 22 +++++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/datadog-js-zstd/src/lib.rs b/crates/datadog-js-zstd/src/lib.rs index 4896301..fed9b1c 100644 --- a/crates/datadog-js-zstd/src/lib.rs +++ b/crates/datadog-js-zstd/src/lib.rs @@ -1,11 +1,8 @@ -use wasm_bindgen::prelude::*; use js_sys::Uint8Array; +use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub fn zstd_compress( - data: Uint8Array, - level: i32, -) -> Uint8Array { +pub fn zstd_compress(data: Uint8Array, level: i32) -> Uint8Array { let vecdata = data.to_vec(); let compressed_data = zstd::encode_all(&vecdata[..], level).expect("Failed to compress data"); Uint8Array::from(compressed_data.as_slice()) diff --git a/crates/library_config/Cargo.toml b/crates/library_config/Cargo.toml index 80093eb..be76bd4 100644 --- a/crates/library_config/Cargo.toml +++ b/crates/library_config/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = "1" -datadog-library-config = { git = "https://github.com/DataDog/libdatadog.git", tag = "v18.1.0" } +libdd-library-config = { git = "https://github.com/DataDog/libdatadog.git", rev = "353134770b312b7ccd2df6afabc253090b948e5f" } wasm-bindgen = "0.2.100" serde = { version = "1.0", features = ["derive"] } diff --git a/crates/library_config/src/lib.rs b/crates/library_config/src/lib.rs index c993fbc..6896a63 100644 --- a/crates/library_config/src/lib.rs +++ b/crates/library_config/src/lib.rs @@ -2,7 +2,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct JsConfigurator { - configurator: Box, + configurator: Box, envp: Vec, args: Vec, } @@ -49,7 +49,7 @@ impl JsConfigurator { #[wasm_bindgen(constructor)] pub fn new() -> Self { JsConfigurator { - configurator: Box::new(datadog_library_config::Configurator::new(false)), // No debug log as WASM can't write to stdout + configurator: Box::new(libdd_library_config::Configurator::new(false)), // No debug log as WASM can't write to stdout envp: Vec::new(), args: Vec::new(), } @@ -70,13 +70,13 @@ impl JsConfigurator { #[wasm_bindgen] pub fn get_config_local_path(&self, target: String) -> Result { let target_enum = match target.as_str() { - "linux" => datadog_library_config::Target::Linux, - "win32" => datadog_library_config::Target::Windows, - "darwin" => datadog_library_config::Target::Macos, + "linux" => libdd_library_config::Target::Linux, + "win32" => libdd_library_config::Target::Windows, + "darwin" => libdd_library_config::Target::Macos, _ => return Err(JsValue::from_str("Unsupported target")), }; Ok( - datadog_library_config::Configurator::local_stable_configuration_path(target_enum) + libdd_library_config::Configurator::local_stable_configuration_path(target_enum) .to_string(), ) } @@ -84,13 +84,13 @@ impl JsConfigurator { #[wasm_bindgen] pub fn get_config_managed_path(&self, target: String) -> Result { let target_enum = match target.as_str() { - "linux" => datadog_library_config::Target::Linux, - "win32" => datadog_library_config::Target::Windows, - "darwin" => datadog_library_config::Target::Macos, + "linux" => libdd_library_config::Target::Linux, + "win32" => libdd_library_config::Target::Windows, + "darwin" => libdd_library_config::Target::Macos, _ => return Err(JsValue::from_str("Unsupported target")), }; Ok( - datadog_library_config::Configurator::fleet_stable_configuration_path(target_enum) + libdd_library_config::Configurator::fleet_stable_configuration_path(target_enum) .to_string(), ) } @@ -108,7 +108,7 @@ impl JsConfigurator { let res_config = self.configurator.get_config_from_bytes( config_string_local.as_bytes(), config_string_managed.as_bytes(), - datadog_library_config::ProcessInfo { + libdd_library_config::ProcessInfo { envp: envp, args: args, language: b"nodejs".to_vec(), From f955722eb94ed15f5736a67323d2434732a95d20 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 25 Jun 2026 15:55:07 -0400 Subject: [PATCH 06/12] feat(pipeline): add meta_struct span bindings libdatadog's Span already carries meta_struct (VecMap) and the exporter serializes it, but no JS binding existed, so structured per-span data (AppSec, Code Origin, Dynamic Instrumentation) could not be sent on the native path. There is no change-buffer opcode for meta_struct, so setMetaStruct writes the value directly onto the span via span_mut() after draining the change queue. meta_struct depends on no other queued op, so bypassing the queue ordering is safe \u2014 subsequent ops are applied on the next flush and never touch meta_struct. getMetaStruct mirrors the existing per-span getters for round-trip coverage. --- crates/pipeline/src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ test/pipeline.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 5a2b7b3..63d0c9c 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -467,6 +467,40 @@ impl WasmSpanState { Ok(span.name.to_string()) } + // `meta_struct` carries msgpack-encoded structured data (e.g. AppSec, Code + // Origin, Dynamic Instrumentation). There is no change-buffer opcode for it, + // so the value is written directly onto the span after draining the queue — + // meta_struct does not depend on any other queued op, so bypassing the queue + // ordering is safe (subsequent ops are applied on the next flush and never + // touch meta_struct). + #[wasm_bindgen(js_name = "setMetaStruct")] + pub fn set_meta_struct( + &self, + span_id: u64, + key: &str, + value: &[u8], + ) -> Result<(), JsValue> { + self.flush_change_queue()?; + let mut cbs = self.cbs.borrow_mut(); + let span = cbs + .span_mut(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + span.meta_struct + .insert(key.into(), span_bytes::SpanBytesImpl(value.to_vec())); + Ok(()) + } + + #[wasm_bindgen(js_name = "getMetaStruct")] + pub fn get_meta_struct(&self, span_id: u64, key: &str) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs.get_span(span_id).map_err(|e| JsValue::from_str(&e.to_string()))?; + // VecMap::get accepts &str directly (SpanString: Borrow). + Ok(span.meta_struct.get(key) + .map(|v| JsValue::from(js_sys::Uint8Array::from(v.0.as_slice()))) + .unwrap_or(JsValue::NULL)) + } + // Trace-level attributes live on the Segment (keyed by segment_id, which // JS allocates and shares across spans in the same local trace). #[wasm_bindgen(js_name = "getTraceMetaAttr")] diff --git a/test/pipeline.js b/test/pipeline.js index e627ffd..22eddaf 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -81,6 +81,15 @@ class Span { return this.nativeSpans.state.getTraceOrigin(this.segmentId) } + setMetaStruct (key, bytes) { + this.nativeSpans.state.setMetaStruct(this.spanIdBig, key, bytes) + return this + } + + getMetaStruct (key) { + return this.nativeSpans.state.getMetaStruct(this.spanIdBig, key) + } + finish () { this.duration = BigInt(Date.now()) * 1000000n - this._startTime return this @@ -382,6 +391,29 @@ describe('pipeline', () => { }) }) + describe('meta_struct', () => { + it('round-trips raw bytes by key', () => { + const span = nativeSpans.createSpan() + const value = new Uint8Array([0x82, 0xa1, 0x61, 0x01, 0xa1, 0x62, 0x02]) + span.setMetaStruct('appsec', value) + + assert.deepStrictEqual(span.getMetaStruct('appsec'), value) + }) + + it('returns null for an unset meta_struct key', () => { + const span = nativeSpans.createSpan() + assert.strictEqual(span.getMetaStruct('missing'), null) + }) + + it('overwrites an existing key on repeated set', () => { + const span = nativeSpans.createSpan() + span.setMetaStruct('k', new Uint8Array([1, 2, 3])) + span.setMetaStruct('k', new Uint8Array([9])) + + assert.deepStrictEqual(span.getMetaStruct('k'), new Uint8Array([9])) + }) + }) + describe('span timing', () => { it('should set and get start time', () => { const span = nativeSpans.createSpan() From 0a937ed3339353146ddf8ec647cb39a13de0d5f8 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 25 Jun 2026 16:02:39 -0400 Subject: [PATCH 07/12] feat(capabilities): support unix socket and named-pipe agent transport The wasm HTTP transport only spoke http/https to a host:port, so a `unix://` or `windows:` agent URL could not be reached: request() treated the hex-encoded socket path (ddcommon's parse_uri stores it in the URI authority) as a TCP host. Detect the unix/windows scheme in request(), hex-decode the socket path from the authority, and pass it to the JS transport, which now uses Node's { socketPath } (covering Windows named pipes too). TCP requests are unchanged; socket requests send a localhost Host header with no port. Adds a unix-socket case to the transport tests. --- crates/capabilities/src/http.rs | 85 +++++++++++++++++++---- crates/capabilities/src/http_transport.js | 17 +++-- test/http_transport.js | 44 +++++++++++- 3 files changed, 125 insertions(+), 21 deletions(-) diff --git a/crates/capabilities/src/http.rs b/crates/capabilities/src/http.rs index 5955b34..8b3ad03 100644 --- a/crates/capabilities/src/http.rs +++ b/crates/capabilities/src/http.rs @@ -30,6 +30,7 @@ extern "C" { host: &str, port: u16, is_https: bool, + socket_path: &str, head_ptr: *const u8, head_len: u32, body_ptr: *const u8, @@ -65,24 +66,41 @@ impl HttpClientCapability for DefaultHttpClient { ) -> impl Future, HttpError>> + MaybeSend { async move { let scheme = req.uri().scheme_str().unwrap_or("http"); - let is_https = scheme == "https"; - let host = req - .uri() - .host() - .ok_or_else(|| HttpError::InvalidRequest(anyhow::anyhow!("missing host in URI")))? - .to_owned(); - let port = req - .uri() - .port_u16() - .unwrap_or(if is_https { 443 } else { 80 }); - - let head = serialize_request_head(&req, &host, port, is_https)?; + + // Unix domain socket / Windows named pipe: ddcommon's `parse_uri` + // hex-encodes the socket path into the URI authority (there is no + // standard URL form for socket paths). On wasm the request bypasses + // ddcommon's native (hyper) connector and reaches us directly, so we + // decode the path here and route over the socket instead of TCP. + let (host, port, is_https, socket_path) = if scheme == "unix" || scheme == "windows" { + (String::new(), 0u16, false, decode_socket_path(req.uri())?) + } else { + let is_https = scheme == "https"; + let host = req.uri().host().ok_or_else(|| { + HttpError::InvalidRequest(anyhow::anyhow!("missing host in URI")) + })?; + let port = req + .uri() + .port_u16() + .unwrap_or(if is_https { 443 } else { 80 }); + (host.to_owned(), port, is_https, String::new()) + }; + + // For a socket request there is no meaningful network host; HTTP/1.1 + // still requires a Host header, so send a stable placeholder (the + // agent does not validate Host over a socket). + let head = if socket_path.is_empty() { + serialize_request_head(&req, &host, port, is_https, false)? + } else { + serialize_request_head(&req, "localhost", port, is_https, true)? + }; let body = req.into_body(); let result = JsFuture::from(http_request( &host, port, is_https, + &socket_path, head.as_ptr(), head.len() as u32, body.as_ptr(), @@ -155,16 +173,51 @@ fn parse_response_headers(header_js: Array) -> Result Result { + let authority = uri + .authority() + .ok_or_else(|| HttpError::InvalidRequest(anyhow::anyhow!("socket URI missing authority")))? + .as_str(); + let bytes = hex_decode(authority).ok_or_else(|| { + HttpError::InvalidRequest(anyhow::anyhow!("socket path authority is not valid hex")) + })?; + String::from_utf8(bytes) + .map_err(|e| HttpError::InvalidRequest(anyhow::anyhow!("socket path is not utf-8: {e}"))) +} + +/// Minimal hex decoder for the socket-path authority. Returns `None` on any +/// malformed input (odd length or non-hex digit) rather than panicking. +fn hex_decode(s: &str) -> Option> { + let bytes = s.as_bytes(); + if !bytes.len().is_multiple_of(2) { + return None; + } + let mut out = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let hi = (pair[0] as char).to_digit(16)?; + let lo = (pair[1] as char).to_digit(16)?; + out.push((hi * 16 + lo) as u8); + } + Some(out) +} + /// Serialize the full HTTP/1.1 request head (request line + Host + Content-Length /// + user headers + terminating CRLF) into a contiguous byte buffer. /// /// The buffer is handed to JS by pointer; JS assigns it to /// `req._header`, bypassing Node's `_storeHeader` serialization. +/// +/// `is_socket` requests (unix socket / named pipe) omit the `:port` suffix on +/// the Host header — there is no TCP port for a socket transport. fn serialize_request_head( req: &http::Request, host: &str, port: u16, is_https: bool, + is_socket: bool, ) -> Result, HttpError> { let method = req.method().as_str(); let path_and_query = req @@ -184,9 +237,11 @@ fn serialize_request_head( buf.extend_from_slice(b"Host: "); buf.extend_from_slice(host.as_bytes()); - let default_port = if is_https { 443 } else { 80 }; - if port != default_port { - write!(&mut buf, ":{port}").map_err(|e| HttpError::Other(e.into()))?; + if !is_socket { + let default_port = if is_https { 443 } else { 80 }; + if port != default_port { + write!(&mut buf, ":{port}").map_err(|e| HttpError::Other(e.into()))?; + } } buf.extend_from_slice(b"\r\n"); diff --git a/crates/capabilities/src/http_transport.js b/crates/capabilities/src/http_transport.js index bbc8267..1c01bb3 100644 --- a/crates/capabilities/src/http_transport.js +++ b/crates/capabilities/src/http_transport.js @@ -26,8 +26,12 @@ module.exports.setResponseHeaderObserver = function (new_observer) { responseHeaderObserver = new_observer; } -module.exports.httpRequest = function (host, port, isHttps, head_ptr, head_len, body_ptr, body_len, wasm_memory) { - const transport = isHttps ? https : http; +module.exports.httpRequest = function (host, port, isHttps, socketPath, head_ptr, head_len, body_ptr, body_len, wasm_memory) { + // A non-empty socketPath routes over a Unix domain socket (or Windows named + // pipe) instead of TCP. Sockets are always plaintext HTTP/1.1, so https is + // ignored in that mode. + const useSocket = typeof socketPath === 'string' && socketPath.length > 0; + const transport = useSocket ? http : (isHttps ? https : http); function isDetachedBufferError(err) { return err instanceof TypeError && /detached/i.test(err.message); @@ -41,9 +45,12 @@ module.exports.httpRequest = function (host, port, isHttps, head_ptr, head_len, const headView = new Uint8Array(wasm_memory.buffer, head_ptr, head_len); const bodyView = new Uint8Array(wasm_memory.buffer, body_ptr, body_len); - // host/port drive socket selection; method/path/headers are placeholders - // because we replace the rendered head below. - const req = transport.request({ host, port, method: 'POST', path: '/' }, (res) => { + // host/port (or socketPath) drive connection selection; method/path/ + // headers are placeholders because we replace the rendered head below. + const requestOptions = useSocket + ? { socketPath, method: 'POST', path: '/' } + : { host, port, method: 'POST', path: '/' }; + const req = transport.request(requestOptions, (res) => { const chunks = []; res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { diff --git a/test/http_transport.js b/test/http_transport.js index 762f5c5..82ad7c4 100644 --- a/test/http_transport.js +++ b/test/http_transport.js @@ -9,6 +9,9 @@ const { describe, it, before, after, beforeEach } = require('node:test') const assert = require('node:assert') const http = require('node:http') +const os = require('node:os') +const path = require('node:path') +const fs = require('node:fs') const transport = require('../crates/capabilities/src/http_transport.js') @@ -52,7 +55,8 @@ describe('http_transport response header observer', () => { 'utf8' ) // head occupies [0, head.length); body is empty (offset 0, length 0). - return transport.httpRequest('127.0.0.1', port, false, 0, head.length, 0, 0, fakeWasmMemory(head)) + // Empty socketPath -> TCP transport. + return transport.httpRequest('127.0.0.1', port, false, '', 0, head.length, 0, 0, fakeWasmMemory(head)) } it('invokes the observer with the raw response headers', async () => { @@ -113,3 +117,41 @@ describe('http_transport response header observer', () => { assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY) }) }) + +// Unix-domain-socket transport: a non-empty socketPath must route the request +// over the socket instead of TCP. Skipped on Windows (no AF_UNIX path here). +describe('http_transport unix socket', { skip: process.platform === 'win32' }, () => { + let server + let socketPath + + before(async () => { + socketPath = path.join(os.tmpdir(), `libdd-uds-test-${process.pid}-${Date.now()}.sock`) + try { fs.unlinkSync(socketPath) } catch {} + server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + res.end(RESPONSE_BODY) + }) + }) + await new Promise((resolve) => server.listen(socketPath, resolve)) + }) + + after(() => new Promise((resolve) => server.close(() => { + try { fs.unlinkSync(socketPath) } catch {} + resolve() + }))) + + it('delivers the request over a unix socket and returns the response', async () => { + const head = Buffer.from( + 'POST /v0.4/traces HTTP/1.1\r\nHost: localhost\r\n' + + 'Content-Length: 0\r\nConnection: close\r\n\r\n', + 'utf8' + ) + // host/port empty/0; socketPath drives the connection. + const [status, , body] = await transport.httpRequest( + '', 0, false, socketPath, 0, head.length, 0, 0, fakeWasmMemory(head) + ) + assert.strictEqual(status, 200) + assert.strictEqual(Buffer.from(body).toString('utf8'), RESPONSE_BODY) + }) +}) From 89c032687133f33770897e7f60761f25501caa9e Mon Sep 17 00:00:00 2001 From: Bryan English Date: Thu, 25 Jun 2026 17:00:53 -0400 Subject: [PATCH 08/12] feat(pipeline): add span_events bindings Add `addSpanEvent` to append OpenTelemetry-style span events onto the top-level v0.4 `span_events` field that libdatadog already serializes. Like meta_struct there is no change-buffer opcode, so the event is appended directly to the span after draining the queue (span_events do not depend on any other queued op, so bypassing queue ordering is safe). Attributes arrive as a flat little-endian buffer with per-value type tags (String=0, Boolean=1, Integer=2, Double=3, Array=4) matching libdatadog's AttributeArrayValue discriminants; every read is bounded against the buffer so a malformed/truncated buffer errors instead of panicking. A `getSpanEventsJson` helper serializes events via the same serde impl used for the msgpack wire format, exercised by new round-trip tests covering each scalar type, arrays, and bounds. --- Cargo.lock | 1 + crates/pipeline/Cargo.toml | 1 + crates/pipeline/src/lib.rs | 154 ++++++++++++++++++++++++++++++ crates/pipeline/src/trace_data.rs | 6 +- test/pipeline.js | 136 ++++++++++++++++++++++++++ 5 files changed, 297 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2c8d4f1..f9b1a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1747,6 +1747,7 @@ dependencies = [ "libdd-trace-utils", "rmp-serde", "serde", + "serde_json", "uuid", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/crates/pipeline/Cargo.toml b/crates/pipeline/Cargo.toml index f7d9935..514e411 100644 --- a/crates/pipeline/Cargo.toml +++ b/crates/pipeline/Cargo.toml @@ -12,6 +12,7 @@ wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" serde = { version = "1.0", features = ["derive"] } +serde_json = "1" libdatadog-nodejs-capabilities = { path = "../capabilities" } libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 63d0c9c..0488e07 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -18,6 +18,9 @@ use trace_data::*; mod stats; use libdd_trace_utils::change_buffer::{ChangeBuffer, ChangeBufferState}; +use libdd_trace_utils::span::v04::{AttributeAnyValue, AttributeArrayValue, SpanEvent}; +use span_string::SpanString; +use std::collections::HashMap; mod utils; use utils::*; @@ -27,6 +30,112 @@ fn init() { console_error_panic_hook::set_once(); } +// --- span event attribute decoding --- +// +// `addSpanEvent` receives its attributes as a flat little-endian buffer built +// by dd-trace-js. Layout: repeated entries until the buffer is exhausted, each +// [key_len: u32][key: utf8][tag: u8] + value +// where the value depends on `tag`: +// 0 String [len: u32][utf8] +// 1 Boolean [u8 (0/1)] +// 2 Integer [i64] +// 3 Double [f64] +// 4 Array [count: u32] then `count` items, each [item_tag: u8][scalar] +// (item_tag must be 0..=3; nested arrays are rejected) +// The tags mirror libdatadog's `AttributeArrayValue` discriminants +// (String=0, Boolean=1, Integer=2, Double=3, Array=4). Every read is bounded +// against the buffer so a malformed/truncated buffer errors instead of +// panicking (matching the hardening in `stringTableInsertMany`/`prepareChunk`). + +fn se_need(buf: &[u8], idx: usize, n: usize) -> Result<(), JsValue> { + // Avoid `idx + n` overflowing: on wasm32 `usize` is 32-bit, and `n` can be + // a u32-derived length (e.g. a crafted `key_len`) near `usize::MAX`, which + // would wrap and let a too-large read slip past the bound and trap on the + // slice. `idx` never exceeds `buf.len()` (it only advances after a checked + // read), so `buf.len() - idx` is the safe remaining-byte form. + if idx > buf.len() || n > buf.len() - idx { + return Err(JsValue::from_str( + "addSpanEvent: truncated span-event attribute buffer", + )); + } + Ok(()) +} + +fn se_read_u8(buf: &[u8], idx: &mut usize) -> Result { + se_need(buf, *idx, 1)?; + let b = buf[*idx]; + *idx += 1; + Ok(b) +} + +fn se_read_u32(buf: &[u8], idx: &mut usize) -> Result { + se_need(buf, *idx, 4)?; + Ok(get_num(buf, idx)) +} + +fn se_read_str(buf: &[u8], idx: &mut usize) -> Result { + let len = se_read_u32(buf, idx)? as usize; + se_need(buf, *idx, len)?; + let s = std::str::from_utf8(&buf[*idx..*idx + len]) + .map_err(|e| JsValue::from_str(&format!("addSpanEvent: invalid utf8: {e}")))?; + *idx += len; + Ok(s.into()) +} + +fn se_read_scalar( + buf: &[u8], + idx: &mut usize, + tag: u8, +) -> Result, JsValue> { + match tag { + 0 => Ok(AttributeArrayValue::String(se_read_str(buf, idx)?)), + 1 => Ok(AttributeArrayValue::Boolean(se_read_u8(buf, idx)? != 0)), + 2 => { + se_need(buf, *idx, 8)?; + Ok(AttributeArrayValue::Integer(get_num(buf, idx))) + } + 3 => { + se_need(buf, *idx, 8)?; + Ok(AttributeArrayValue::Double(get_num(buf, idx))) + } + _ => Err(JsValue::from_str( + "addSpanEvent: invalid span-event attribute tag", + )), + } +} + +fn decode_span_event_attributes( + buf: &[u8], +) -> Result>, JsValue> { + let mut attributes = HashMap::new(); + let mut idx = 0usize; + while idx < buf.len() { + let key = se_read_str(buf, &mut idx)?; + let tag = se_read_u8(buf, &mut idx)?; + let value = if tag == 4 { + let count = se_read_u32(buf, &mut idx)? as usize; + // Each item is at least 1 byte (its tag), so cap the pre-allocation + // to the remaining buffer: an inflated count can't force a huge + // allocation, and the per-item bounded reads catch truncation. + let mut items = Vec::with_capacity(count.min(buf.len().saturating_sub(idx))); + for _ in 0..count { + let item_tag = se_read_u8(buf, &mut idx)?; + if item_tag == 4 { + return Err(JsValue::from_str( + "addSpanEvent: nested arrays are not supported", + )); + } + items.push(se_read_scalar(buf, &mut idx, item_tag)?); + } + AttributeAnyValue::Array(items) + } else { + AttributeAnyValue::SingleValue(se_read_scalar(buf, &mut idx, tag)?) + }; + attributes.insert(key, value); + } + Ok(attributes) +} + #[wasm_bindgen] /// All mutable state is behind RefCell to allow `&self` methods on the /// wasm-bindgen wrapper. This prevents re-entrant borrow panics when: @@ -501,6 +610,51 @@ impl WasmSpanState { .unwrap_or(JsValue::NULL)) } + // Span events (OpenTelemetry-style) are serialized by libdatadog as the + // top-level v0.4 `span_events` field when present. Like meta_struct there + // is no change-buffer opcode, so the event is appended directly to the span + // after draining the queue (span_events do not depend on any other queued + // op, so bypassing queue ordering is safe). `attrs_buf` is the flat typed + // attribute encoding decoded by `decode_span_event_attributes`. + #[wasm_bindgen(js_name = "addSpanEvent")] + pub fn add_span_event( + &self, + span_id: u64, + name: &str, + time_unix_nano: u64, + attrs_buf: &[u8], + ) -> Result<(), JsValue> { + self.flush_change_queue()?; + // Decode before borrowing cbs mutably so a malformed buffer errors + // without holding the borrow. + let attributes = decode_span_event_attributes(attrs_buf)?; + let mut cbs = self.cbs.borrow_mut(); + let span = cbs + .span_mut(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + span.span_events.push(SpanEvent { + time_unix_nano, + name: name.into(), + attributes, + }); + Ok(()) + } + + // Test/inspection helper: serialize the span's events to JSON via the same + // serde `Serialize` impl libdatadog uses for the msgpack wire format, so + // the `type`/`*_value` shape mirrors exactly what is sent to the agent + // (String=0, Boolean=1, Integer=2, Double=3, Array=4). + #[wasm_bindgen(js_name = "getSpanEventsJson")] + pub fn get_span_events_json(&self, span_id: u64) -> Result { + self.flush_change_queue()?; + let cbs = self.cbs.borrow(); + let span = cbs + .get_span(span_id) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + serde_json::to_string(&span.span_events) + .map_err(|e| JsValue::from_str(&format!("getSpanEventsJson: {e}"))) + } + // Trace-level attributes live on the Segment (keyed by segment_id, which // JS allocates and shares across spans in the same local trace). #[wasm_bindgen(js_name = "getTraceMetaAttr")] diff --git a/crates/pipeline/src/trace_data.rs b/crates/pipeline/src/trace_data.rs index 2546477..94cbcfe 100644 --- a/crates/pipeline/src/trace_data.rs +++ b/crates/pipeline/src/trace_data.rs @@ -1,9 +1,13 @@ use libdd_trace_utils::span::TraceData; +use serde::Serialize; use crate::span_bytes::SpanBytesImpl; use crate::span_string::SpanString; -#[derive(Clone, Default, Debug, PartialEq)] +// `Serialize` is derived only so the test helper `getSpanEventsJson` can +// serialize `Vec>` (serde's derive on the generic +// `SpanEvent` requires `T: Serialize`). The unit struct carries no data. +#[derive(Clone, Default, Debug, PartialEq, Serialize)] pub struct WasmTraceData; impl TraceData for WasmTraceData { diff --git a/test/pipeline.js b/test/pipeline.js index 22eddaf..cc333be 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -90,6 +90,20 @@ class Span { return this.nativeSpans.state.getMetaStruct(this.spanIdBig, key) } + addSpanEvent (name, timeUnixNano, attributes = {}) { + this.nativeSpans.state.addSpanEvent( + this.spanIdBig, + name, + BigInt(timeUnixNano), + encodeSpanEventAttrs(attributes) + ) + return this + } + + getSpanEvents () { + return JSON.parse(this.nativeSpans.state.getSpanEventsJson(this.spanIdBig)) + } + finish () { this.duration = BigInt(Date.now()) * 1000000n - this._startTime return this @@ -296,6 +310,41 @@ class NativeSpansInterface { } } +// Build the flat span-event attribute buffer consumed by the Rust decoder +// (`decode_span_event_attributes` in crates/pipeline/src/lib.rs). This mirrors +// what dd-trace-js's `addSpanEvent` wrapper produces. Tags: String=0, +// Boolean=1, Integer=2, Double=3, Array=4 (matching libdatadog's +// AttributeArrayValue discriminants). +function encodeSpanEventAttrs (attributes) { + const enc = new TextEncoder() + const chunks = [] + const u32 = (n) => { const b = Buffer.alloc(4); b.writeUInt32LE(n >>> 0, 0); return b } + const i64 = (n) => { const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(n), 0); return b } + const f64 = (n) => { const b = Buffer.alloc(8); b.writeDoubleLE(n, 0); return b } + const str = (s) => { const sb = Buffer.from(enc.encode(s)); return Buffer.concat([u32(sb.length), sb]) } + // Returns `[tag][value]` — used both for single values and array items. + const scalar = (v) => { + if (typeof v === 'string') return Buffer.concat([Buffer.from([0]), str(v)]) + if (typeof v === 'boolean') return Buffer.concat([Buffer.from([1]), Buffer.from([v ? 1 : 0])]) + if (typeof v === 'number') { + return Number.isInteger(v) + ? Buffer.concat([Buffer.from([2]), i64(v)]) + : Buffer.concat([Buffer.from([3]), f64(v)]) + } + throw new TypeError(`unsupported span-event attribute value: ${typeof v}`) + } + for (const [key, value] of Object.entries(attributes)) { + chunks.push(str(key)) + if (Array.isArray(value)) { + chunks.push(Buffer.from([4]), u32(value.length)) + for (const item of value) chunks.push(scalar(item)) + } else { + chunks.push(scalar(value)) + } + } + return new Uint8Array(Buffer.concat(chunks)) +} + describe('pipeline', () => { let nativeSpans @@ -414,6 +463,93 @@ describe('pipeline', () => { }) }) + describe('span_events', () => { + it('appends an event with no attributes', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('exception', 1727211691770716000n) + + const events = span.getSpanEvents() + assert.strictEqual(events.length, 1) + assert.strictEqual(events[0].name, 'exception') + assert.strictEqual(events[0].time_unix_nano, 1727211691770716000) + // Empty attributes are skipped by libdatadog's serializer. + assert.strictEqual(events[0].attributes, undefined) + }) + + it('round-trips scalar attributes of every type with correct type tags', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('evt', 1000n, { + s: 'hello', + b: true, + i: 42, + d: 3.5 + }) + + const [event] = span.getSpanEvents() + assert.strictEqual(event.name, 'evt') + assert.strictEqual(event.time_unix_nano, 1000) + // type tags: String=0, Boolean=1, Integer=2, Double=3 + assert.deepStrictEqual(event.attributes.s, { type: 0, string_value: 'hello' }) + assert.deepStrictEqual(event.attributes.b, { type: 1, bool_value: true }) + assert.deepStrictEqual(event.attributes.i, { type: 2, int_value: 42 }) + assert.deepStrictEqual(event.attributes.d, { type: 3, double_value: 3.5 }) + }) + + it('round-trips an array attribute (type 4) with typed items', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('evt', 1n, { tags: ['a', 'b'], nums: [1, 2, 3] }) + + const [event] = span.getSpanEvents() + assert.deepStrictEqual(event.attributes.tags, { + type: 4, + array_value: { values: [{ type: 0, string_value: 'a' }, { type: 0, string_value: 'b' }] } + }) + assert.deepStrictEqual(event.attributes.nums, { + type: 4, + array_value: { values: [{ type: 2, int_value: 1 }, { type: 2, int_value: 2 }, { type: 2, int_value: 3 }] } + }) + }) + + it('appends multiple events in order', () => { + const span = nativeSpans.createSpan() + span.addSpanEvent('first', 1n) + span.addSpanEvent('second', 2n, { k: 'v' }) + + const events = span.getSpanEvents() + assert.strictEqual(events.length, 2) + assert.strictEqual(events[0].name, 'first') + assert.strictEqual(events[1].name, 'second') + assert.deepStrictEqual(events[1].attributes.k, { type: 0, string_value: 'v' }) + }) + + it('returns an empty array for a span with no events', () => { + const span = nativeSpans.createSpan() + assert.deepStrictEqual(span.getSpanEvents(), []) + }) + + it('rejects a truncated attribute buffer instead of panicking', () => { + const span = nativeSpans.createSpan() + // key_len=5 but no key bytes follow → bounded read must error. + const bad = new Uint8Array([5, 0, 0, 0]) + assert.throws( + () => span.nativeSpans.state.addSpanEvent(span.spanIdBig, 'evt', 1n, bad), + /truncated span-event attribute buffer/ + ) + }) + + it('rejects an overflowing key_len without trapping (wasm32 usize)', () => { + const span = nativeSpans.createSpan() + // key_len = 0xFFFFFFFF: on wasm32 `idx + key_len` would wrap and slip + // past the bound, trapping on the slice. The remaining-byte form must + // reject it as a truncated buffer instead. + const bad = new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00]) + assert.throws( + () => span.nativeSpans.state.addSpanEvent(span.spanIdBig, 'evt', 1n, bad), + /truncated span-event attribute buffer/ + ) + }) + }) + describe('span timing', () => { it('should set and get start time', () => { const span = nativeSpans.createSpan() From 9fbeecd045aedb96daab6ff0d26453b402340af9 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Fri, 26 Jun 2026 15:59:43 -0400 Subject: [PATCH 09/12] feat(pipeline): add v0.5 output format selection Add setUseV05() to WasmSpanState so the single trace exporter can emit the v0.5 wire format (/v0.5/traces) instead of the default v0.4. The flag is read once, at the lazy exporter build on first send, then fixed; callers must set it before the first flush. v0.5 uses a fixed 12-field schema with no slots for meta_struct, span_events, or span_links, so libdatadog's v0.5 serializer silently drops them. This mirrors dd-trace-js master's v0.5 encoder and is intentional \u2014 there is no guard and no dual exporter. libdatadog does not downgrade V05 (unlike V1), so the caller (dd-trace-js) is responsible for only enabling this after the agent advertises /v0.5/traces via /info. --- crates/pipeline/src/lib.rs | 36 ++++++++++++++++++++++++++-- test/pipeline.js | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 0488e07..78d4969 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -1,6 +1,8 @@ use libdatadog_nodejs_capabilities::WasmCapabilities; use libdd_data_pipeline::trace_exporter::agent_response::AgentResponse; -use libdd_data_pipeline::trace_exporter::{TraceExporter, TraceExporterBuilder}; +use libdd_data_pipeline::trace_exporter::{ + TraceExporter, TraceExporterBuilder, TraceExporterOutputFormat, +}; use libdd_shared_runtime::LocalRuntime; use std::cell::{Cell, RefCell, UnsafeCell}; use std::ffi::CStr; @@ -168,6 +170,16 @@ pub struct WasmSpanState { /// alias across the await (UB). The guard makes a re-entrant call return /// an error instead. sending: Cell, + /// When true, the lazily-built exporter is configured for v0.5 output + /// (`/v0.5/traces`) instead of the default v0.4. v0.5 is a smaller, fixed + /// 12-field schema with NO slots for `meta_struct`/`span_events`/`span_links`, + /// so libdatadog's v0.5 serializer silently drops them — this mirrors + /// dd-trace-js master's v0.5 encoder and is intentional. Caller (dd-trace-js) + /// must only enable this after confirming the agent advertises `/v0.5/traces` + /// (libdd does NOT downgrade V05 the way it does V1). The output format is + /// fixed once the exporter is built on the first send, so `setUseV05` only + /// takes effect if called before then. + use_v05: Cell, } /// Clears an in-flight flag on drop, so an early return or a dropped future @@ -256,9 +268,22 @@ impl WasmSpanState { stats_collector: RefCell::new(stats_collector), prepared_spans: RefCell::new(None), sending: Cell::new(false), + use_v05: Cell::new(false), }) } + /// Select v0.5 output for the trace exporter. Must be called before the + /// first `sendPreparedChunk` (the exporter is built lazily on first send and + /// the output format is fixed at build time; later calls have no effect). + /// + /// v0.5 silently drops `meta_struct` (and top-level `span_events`/`span_links`) + /// because the v0.5 wire schema has no slots for them — the caller is + /// responsible for only enabling this when the agent supports `/v0.5/traces`. + #[wasm_bindgen(js_name = "setUseV05")] + pub fn set_use_v05(&self, v: bool) { + self.use_v05.set(v); + } + #[wasm_bindgen] pub fn change_queue_ptr(&self) -> *const u8 { self.change_queue.as_ptr() @@ -373,9 +398,16 @@ impl WasmSpanState { // First send: build the exporter asynchronously. `build` is not // available on wasm (it needs a blocking runtime), so we drive // `build_async` here where we already have an async context. - let builder = unsafe { &mut *self.builder.get() } + let mut builder = unsafe { &mut *self.builder.get() } .take() .ok_or_else(|| JsValue::from_str("exporter builder already consumed"))?; + // Output format is decided here, at first build, and then fixed. + // v0.5 drops meta_struct/span_events/span_links by design (the v0.5 + // schema has no slots for them); dd-trace-js only enables this after + // confirming agent `/v0.5/traces` support via `/info`. + if self.use_v05.get() { + builder.set_output_format(TraceExporterOutputFormat::V05); + } let built = builder .build_async::() .await diff --git a/test/pipeline.js b/test/pipeline.js index cc333be..13259ea 100644 --- a/test/pipeline.js +++ b/test/pipeline.js @@ -781,6 +781,54 @@ describe('pipeline', () => { }) }) + describe('v0.5 output format', () => { + // Spin up a mock agent that records the request path, so we can assert the + // exporter targets /v0.4/traces by default and /v0.5/traces after + // setUseV05(true). (v0.5 itself drops meta_struct/span_events by design; + // here we only verify endpoint routing, which is the observable behavior.) + async function flushAndCapturePath (useV05) { + const http = require('node:http') + const seen = [] + const server = http.createServer((req, res) => { + req.on('data', () => {}) + req.on('end', () => { + seen.push({ method: req.method, url: req.url }) + res.writeHead(200, { 'content-type': 'application/json' }) + res.end('{}') + }) + }) + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)) + const { port } = server.address() + const ns = new NativeSpansInterface({ agentUrl: `http://127.0.0.1:${port}` }) + if (useV05) ns.state.setUseV05(true) + const span = ns.createSpan() + span.name = 'v05-span' + span.service = 'test-service' + span.resource = 'test-resource' + span.type = 'web' + span.duration = 1000000n + try { + await ns.flushSpans(span) + return seen.find(r => r.method === 'POST') + } finally { + server.closeAllConnections?.() + server.close() + } + } + + it('targets /v0.4/traces by default', async () => { + const req = await flushAndCapturePath(false) + assert.ok(req, 'agent received a POST') + assert.strictEqual(req.url, '/v0.4/traces') + }) + + it('targets /v0.5/traces after setUseV05(true)', async () => { + const req = await flushAndCapturePath(true) + assert.ok(req, 'agent received a POST') + assert.strictEqual(req.url, '/v0.5/traces') + }) + }) + describe('client-side stats', () => { it('aggregates and flushes stats to /v0.6/stats', async () => { const http = require('node:http') From 7b2d50b74a68559a4b877c1357c6e8ad366d5909 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Mon, 29 Jun 2026 14:05:25 -0400 Subject: [PATCH 10/12] chore: remove unused trace_exporter crate The pipeline crate's WasmSpanState builds its own internal TraceExporter and owns serialization + send (sendPreparedChunk), superseding the standalone trace_exporter binding (JsTraceExporter, the earlier pre-encoded-v0.4-bytes path). Nothing consumes it: dd-trace-js (native-spans and master) loads only the pipeline crate, and no other tracked code references it. Drop the crate, its wasm test, and the build/test wiring. --- Cargo.lock | 17 ----- crates/trace_exporter/Cargo.toml | 25 ------- crates/trace_exporter/src/lib.rs | 114 ------------------------------ package.json | 2 +- scripts/test.sh | 4 -- test/wasm/trace_exporter/index.js | 63 ----------------- 6 files changed, 1 insertion(+), 224 deletions(-) delete mode 100644 crates/trace_exporter/Cargo.toml delete mode 100644 crates/trace_exporter/src/lib.rs delete mode 100644 test/wasm/trace_exporter/index.js diff --git a/Cargo.lock b/Cargo.lock index f9b1a4a..05979ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2406,23 +2406,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" -[[package]] -name = "trace-exporter" -version = "0.1.0" -dependencies = [ - "console_error_panic_hook", - "getrandom 0.2.17", - "js-sys", - "libdatadog-nodejs-capabilities", - "libdd-capabilities 2.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", - "libdd-data-pipeline", - "libdd-shared-runtime 1.0.0 (git+https://github.com/DataDog/libdatadog.git?branch=main)", - "uuid", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test", -] - [[package]] name = "tracing" version = "0.1.44" diff --git a/crates/trace_exporter/Cargo.toml b/crates/trace_exporter/Cargo.toml deleted file mode 100644 index f85b7c3..0000000 --- a/crates/trace_exporter/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "trace-exporter" -version = "0.1.0" -edition = "2021" -description = "Wasm binding for libdd-data-pipeline TraceExporter" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -wasm-bindgen = "0.2" -wasm-bindgen-futures = "0.4" -js-sys = "0.3" -libdatadog-nodejs-capabilities = { path = "../capabilities" } -libdd-capabilities = { git = "https://github.com/DataDog/libdatadog.git", branch = "main" } -libdd-data-pipeline = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } -libdd-shared-runtime = { git = "https://github.com/DataDog/libdatadog.git", branch = "main", default-features = false } -console_error_panic_hook = "0.1" - -[target.'cfg(target_arch = "wasm32")'.dependencies] -getrandom = { version = "0.2", features = ["js"] } -uuid = { version = "1", features = ["js"] } - -[dev-dependencies] -wasm-bindgen-test = "0.3" diff --git a/crates/trace_exporter/src/lib.rs b/crates/trace_exporter/src/lib.rs deleted file mode 100644 index b5cf23d..0000000 --- a/crates/trace_exporter/src/lib.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -//! Wasm binding for `TraceExporter`. -//! -//! This crate exposes libdatadog's trace exporter to JavaScript via -//! `wasm_bindgen`. The generic parameter is pinned to `WasmCapabilities`, -//! which routes I/O (HTTP, etc.) back into JavaScript. -//! -//! This is a lower-level, secondary path that sends pre-encoded v0.4 trace -//! bytes directly; the primary span pipeline (and its richer tracer metadata) -//! lives in the `pipeline` crate's `WasmSpanState`. `JsTraceExporter` hardcodes -//! its language metadata accordingly. - -use std::cell::{Cell, UnsafeCell}; - -use libdatadog_nodejs_capabilities::WasmCapabilities; -use libdd_data_pipeline::trace_exporter::{ - agent_response::AgentResponse, TraceExporter, TraceExporterBuilder, TraceExporterInputFormat, - TraceExporterOutputFormat, -}; -use libdd_shared_runtime::LocalRuntime; -use wasm_bindgen::prelude::*; - -#[wasm_bindgen(start)] -fn init() { - console_error_panic_hook::set_once(); -} - -#[wasm_bindgen] -pub struct JsTraceExporter { - /// Built lazily on first send: `build` is unavailable on wasm (it needs a - /// blocking runtime), so the configured builder is stashed and driven via - /// `build_async` inside the async send path. - // wasm uses the single-threaded LocalRuntime (spawn_local), not the - // native multi-thread runtimes. - inner: UnsafeCell>>, - builder: UnsafeCell>>, - /// Re-entrancy guard for `send`. wasm-bindgen async exports can be - /// re-invoked from JS before the prior future resolves; without this, two - /// calls would each take `&mut` out of `inner`/`builder` and alias across - /// the await (UB). A re-entrant call returns an error instead. - sending: Cell, -} - -// SAFETY: wasm is single-threaded; there are no concurrent accesses. -unsafe impl Send for JsTraceExporter {} -unsafe impl Sync for JsTraceExporter {} - -/// Clears the in-flight flag on drop (covers early returns / dropped futures). -struct InFlightGuard<'a>(&'a Cell); -impl Drop for InFlightGuard<'_> { - fn drop(&mut self) { - self.0.set(false); - } -} - -#[wasm_bindgen] -impl JsTraceExporter { - #[wasm_bindgen(constructor)] - pub fn new(url: &str, service: &str) -> Result { - let mut builder = TraceExporterBuilder::::new(); - builder - .set_url(url) - .set_service(service) - .set_language("javascript") - .set_language_version("") - .set_language_interpreter("nodejs") - .set_input_format(TraceExporterInputFormat::V04) - .set_output_format(TraceExporterOutputFormat::V04); - - Ok(JsTraceExporter { - inner: UnsafeCell::new(None), - builder: UnsafeCell::new(Some(builder)), - sending: Cell::new(false), - }) - } - - #[wasm_bindgen] - pub async fn send(&self, data: &[u8]) -> Result { - if self.sending.get() { - return Err(JsValue::from_str("send is already in flight")); - } - self.sending.set(true); - let _in_flight = InFlightGuard(&self.sending); - - // SAFETY: wasm is single-threaded and the `sending` guard above rejects - // re-entrant calls, so this is the only live reference across the await. - let slot = unsafe { &mut *self.inner.get() }; - if slot.is_none() { - let builder = unsafe { &mut *self.builder.get() } - .take() - .ok_or_else(|| JsValue::from_str("exporter builder already consumed"))?; - let built = builder - .build_async::() - .await - .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; - *slot = Some(built); - } - let inner = slot.as_ref().unwrap(); - - let result = inner - .send_async(data) - .await - .map_err(|e| JsValue::from_str(&format!("{:?}", e)))?; - - // Mirror WasmSpanState.sendPreparedChunk's representation so both send - // paths report an unchanged agent response identically. - match result { - AgentResponse::Changed { body } => Ok(JsValue::from_str(&body)), - AgentResponse::Unchanged => Ok(JsValue::from_str("unchanged")), - } - } -} diff --git a/package.json b/package.json index df07da2..43218af 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build-debug": "mkdir -p target && yarn -s cargo-build > ./target/out.ndjson && yarn -s copy-artifacts", "build-release": "mkdir -p target && yarn -s cargo-build-release > ./target/out.ndjson && yarn -s copy-artifacts", "build-all": "mkdir -p target && yarn -s cargo-build -- --workspace > ./target/out.ndjson && yarn -s copy-artifacts && yarn -s build-wasm", - "build-wasm": "node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js trace_exporter && node scripts/build-wasm.js pipeline", + "build-wasm": "node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js pipeline", "cargo-build-release": "yarn -s cargo-build -- --release", "cargo-build": "cargo build --message-format=json-render-diagnostics", "copy-artifacts": "node ./scripts/copy-artifacts", diff --git a/scripts/test.sh b/scripts/test.sh index 7a33a8b..d85f5cb 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -35,7 +35,3 @@ for d in test/*/; do ;; esac done - -# The wasm trace exporter integration test runs against its own in-process mock -# agent, so run it directly (other test/wasm/* modules remain manual for now). -run_test test/wasm/trace_exporter/index.js diff --git a/test/wasm/trace_exporter/index.js b/test/wasm/trace_exporter/index.js deleted file mode 100644 index f4e2b39..0000000 --- a/test/wasm/trace_exporter/index.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict' - -// This test exercises the full call flow: -// JS -> wasm (TraceExporter logic) -> JS (http_transport.js for I/O) -> wasm -> JS -// -// To run: -// 1. Build: wasm-pack build --target nodejs ./crates/trace_exporter --out-dir ../../prebuilds/trace_exporter -// 2. Run: node test_wasm.js trace_exporter - -const http = require('http') -const loader = require('../../../load.js') -const assert = require('assert') - -const traceExporter = loader.load('trace_exporter') -assert(traceExporter !== undefined, 'trace_exporter wasm module loaded') - -// Start a minimal HTTP server that acts as a mock Datadog agent -let receivedBytes = -1 -const server = http.createServer((req, res) => { - let body = [] - req.on('data', chunk => body.push(chunk)) - req.on('end', () => { - receivedBytes = Buffer.concat(body).length - // Return a minimal agent response - res.writeHead(200, { 'content-type': 'application/json' }) - res.end(JSON.stringify({ rate_by_service: {} })) - }) -}) - -server.listen(0, '127.0.0.1', async () => { - const port = server.address().port - const url = `http://127.0.0.1:${port}` - - try { - // Create a TraceExporter pointing at the mock agent - const exporter = new traceExporter.JsTraceExporter(url, 'test-service') - assert(exporter !== undefined, 'JsTraceExporter created') - - // Send a minimal msgpack-encoded v0.4 trace payload (empty array of traces) - // This is msgpack for [[]] — one trace containing no spans - const payload = new Uint8Array([0x91, 0x90]) - const result = await exporter.send(payload) - - // send() resolves to the agent response body (or the sentinel 'unchanged'); - // either way a successful round-trip yields a truthy string, and the mock - // agent must have actually received the payload bytes. - assert.ok(result, 'send() returned a truthy agent response') - assert.strictEqual(typeof result, 'string', 'send() result is a string') - assert.ok(receivedBytes >= 0, 'mock agent received the trace request') - console.log('Trace export result:', result) - console.log('PASS: wasm trace exporter integration test') - } catch (err) { - console.error('Test error:', err) - process.exitCode = 1 - } finally { - server.close() - } - - // The wasm trace exporter keeps the event loop alive after a send (its - // runtime machinery has no shutdown on wasm), so exit explicitly once the - // assertions are done — mirrors --test-force-exit for the node:test files. - process.exit(process.exitCode || 0) -}) From 2acf0666aa6d87615d72e7f69535bf1c3a246ad7 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Mon, 29 Jun 2026 14:39:06 -0400 Subject: [PATCH 11/12] build: restore wasm-pack install in build-wasm The branch's build-wasm dropped the leading `yarn -s install-wasm-pack` that main still runs, so a clean checkout without wasm-pack on PATH fails with `wasm-pack: not found` before building any module. Restore it. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43218af..c48edb1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build-debug": "mkdir -p target && yarn -s cargo-build > ./target/out.ndjson && yarn -s copy-artifacts", "build-release": "mkdir -p target && yarn -s cargo-build-release > ./target/out.ndjson && yarn -s copy-artifacts", "build-all": "mkdir -p target && yarn -s cargo-build -- --workspace > ./target/out.ndjson && yarn -s copy-artifacts && yarn -s build-wasm", - "build-wasm": "node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js pipeline", + "build-wasm": "yarn -s install-wasm-pack && node scripts/build-wasm.js library_config && node scripts/build-wasm.js datadog-js-zstd && node scripts/build-wasm.js pipeline", "cargo-build-release": "yarn -s cargo-build -- --release", "cargo-build": "cargo build --message-format=json-render-diagnostics", "copy-artifacts": "node ./scripts/copy-artifacts", From 403ad197fc82652f35f23c1d2865750bffc1b9b5 Mon Sep 17 00:00:00 2001 From: Bryan English Date: Mon, 29 Jun 2026 14:39:08 -0400 Subject: [PATCH 12/12] fix(pipeline): keep stats collector available during in-flight flush MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flushStats() took the StatsCollector out of its RefCell for the whole async send, so a prepareChunk() during that await saw None and skipped add_spans — permanently dropping those successfully-sent spans from client-side stats. Split StatsCollector::flush into a synchronous prepare_request (drain + encode under a brief borrow) and an async send_request (no borrow). flushStats now builds the request, releases the collector, then awaits the send, so concurrent add_spans is counted. --- crates/pipeline/src/lib.rs | 34 ++++++++++++++++++++++---------- crates/pipeline/src/stats.rs | 38 ++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 16 deletions(-) diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index 78d4969..2d780c9 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -434,17 +434,31 @@ impl WasmSpanState { /// `force=true` on shutdown. #[wasm_bindgen(js_name = "flushStats")] pub async fn flush_stats(&self, force: bool) -> Result { - // Take the collector out of the RefCell so no borrow is held across the - // await (the send is async). This also guards re-entrancy: a second - // flushStats (or stats access from prepareChunk) during the await sees - // `None` and is a no-op instead of a double-borrow panic. - let mut collector = match self.stats_collector.borrow_mut().take() { - Some(collector) => collector, - None => return Ok(false), + // Build the stats request under a brief *synchronous* borrow, then drop + // the borrow BEFORE the async send. The collector therefore stays in + // `stats_collector`, so a concurrent `prepareChunk` during the in-flight + // send still reaches `add_spans` and those spans are counted. (Taking + // the collector out for the whole await would silently drop them from + // client-side stats.) No borrow is held across the await, so there is + // no double-borrow hazard from overlapping calls. + let req = { + let mut guard = self.stats_collector.borrow_mut(); + match guard.as_mut() { + Some(collector) => collector + .prepare_request(force) + .map_err(|e| JsValue::from_str(&e))?, + None => return Ok(false), + } }; - let result = collector.flush(force).await; - *self.stats_collector.borrow_mut() = Some(collector); - result.map_err(|e| JsValue::from_str(&e)) + match req { + Some(req) => { + stats::StatsCollector::send_request(req) + .await + .map_err(|e| JsValue::from_str(&e))?; + Ok(true) + } + None => Ok(false), + } } /// Flush the queued change-buffer operations. On success always returns diff --git a/crates/pipeline/src/stats.rs b/crates/pipeline/src/stats.rs index e533a2b..7f44bea 100644 --- a/crates/pipeline/src/stats.rs +++ b/crates/pipeline/src/stats.rs @@ -76,14 +76,18 @@ impl StatsCollector { } } - /// Flush aggregated stats and send to the agent. + /// Drain aggregated stats into a ready-to-send request, **synchronously**. /// - /// Returns `Ok(true)` if stats were sent, `Ok(false)` if there was nothing - /// to send, or `Err` on transport failure. - pub async fn flush(&mut self, force: bool) -> Result { + /// Returns `Ok(None)` when there is nothing to flush. The concentrator is + /// drained and the sequence advanced as part of this call, so the returned + /// request must be sent (see `send_request`). Kept synchronous and separate + /// from the send so a caller can build the request under a brief borrow and + /// release the collector *before* the async send — leaving it available for + /// `add_spans` while the stats request is in flight. + pub fn prepare_request(&mut self, force: bool) -> Result>, String> { let buckets = self.concentrator.flush(now(), force); if buckets.is_empty() { - return Ok(false); + return Ok(None); } self.sequence += 1; @@ -106,13 +110,35 @@ impl StatsCollector { .body(Bytes::from(body)) .map_err(|e| format!("failed to build stats request: {e}"))?; + Ok(Some(req)) + } + + /// Send a prepared stats request to the agent. Does **not** borrow the + /// collector, so trace export (`add_spans`) can proceed during the await. + pub async fn send_request(req: http::Request) -> Result<(), String> { let client = DefaultHttpClient::new_client(); client .request(req) .await .map_err(|e| format!("stats send error: {e:?}"))?; + Ok(()) + } - Ok(true) + /// Flush aggregated stats and send to the agent. + /// + /// Returns `Ok(true)` if stats were sent, `Ok(false)` if there was nothing + /// to send, or `Err` on transport failure. Convenience wrapper around + /// `prepare_request` + `send_request`; callers that flush concurrently with + /// trace export should use those two directly so the collector isn't held + /// across the await (see `flushStats`). + pub async fn flush(&mut self, force: bool) -> Result { + match self.prepare_request(force)? { + Some(req) => { + Self::send_request(req).await?; + Ok(true) + } + None => Ok(false), + } } /// Update the agent URL (e.g. after reconfiguration).