diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000..26e2595 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,14 @@ +name: Run Tests + +on: + push: + branches: + - test + +jobs: + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm install + - run: npm test diff --git a/package.json b/package.json index 99ecb8f..ed3edb2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "types": "dist/index.d.ts", "scripts": { "build": "tsup src/index.ts src/cli.ts --format esm,cjs --dts", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "tsx --test test/*.test.ts" }, "bin": { "lmnr": "./dist/cli.js" @@ -48,6 +48,7 @@ "llamaindex": "^0.7.10", "openai": "^4.75.0", "tsup": "^8.3.5", + "tsx": "^4.19.2", "typescript": "^5.7.2" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54a026e..94ea959 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,7 +146,10 @@ importers: version: 4.75.0(encoding@0.1.13)(zod@3.23.8) tsup: specifier: ^8.3.5 - version: 8.3.5(postcss@8.4.47)(typescript@5.7.2)(yaml@2.6.1) + version: 8.3.5(postcss@8.4.47)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.6.1) + tsx: + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.7.2 version: 5.7.2 @@ -398,144 +401,288 @@ packages: resolution: {integrity: sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==} engines: {node: '>=14.17.0'} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.24.0': resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.24.0': resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.24.0': resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.24.0': resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.24.0': resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.24.0': resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.24.0': resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.24.0': resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.24.0': resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.24.0': resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.24.0': resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.24.0': resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.24.0': resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.24.0': resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.24.0': resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.24.0': resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.24.0': resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} engines: {node: '>=18'} cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.24.0': resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-arm64@0.24.0': resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.24.0': resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.24.0': resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.24.0': resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.24.0': resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.24.0': resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} engines: {node: '>=18'} @@ -1803,6 +1950,11 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + esbuild@0.24.0: resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} engines: {node: '>=18'} @@ -1958,6 +2110,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -2753,6 +2908,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -3041,6 +3199,11 @@ packages: typescript: optional: true + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -3942,75 +4105,147 @@ snapshots: '@discoveryjs/json-ext@0.6.3': {} + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/aix-ppc64@0.24.0': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm64@0.24.0': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-arm@0.24.0': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/android-x64@0.24.0': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.24.0': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.24.0': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.24.0': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.24.0': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.24.0': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-arm@0.24.0': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-ia32@0.24.0': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-loong64@0.24.0': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.24.0': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.24.0': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.24.0': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-s390x@0.24.0': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/linux-x64@0.24.0': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.24.0': optional: true + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-arm64@0.24.0': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.24.0': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.24.0': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.24.0': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-ia32@0.24.0': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@esbuild/win32-x64@0.24.0': optional: true @@ -5667,6 +5902,33 @@ snapshots: es-errors@1.3.0: {} + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + esbuild@0.24.0: optionalDependencies: '@esbuild/aix-ppc64': 0.24.0 @@ -5846,6 +6108,10 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + github-from-package@0.0.0: {} glob@10.4.5: @@ -6567,11 +6833,12 @@ snapshots: dependencies: agentkeepalive: 4.5.0 - postcss-load-config@6.0.1(postcss@8.4.47)(yaml@2.6.1): + postcss-load-config@6.0.1(postcss@8.4.47)(tsx@4.19.2)(yaml@2.6.1): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.4.47 + tsx: 4.19.2 yaml: 2.6.1 postcss@8.4.47: @@ -6753,6 +7020,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.8: dependencies: is-core-module: 2.15.1 @@ -7085,7 +7354,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(postcss@8.4.47)(typescript@5.7.2)(yaml@2.6.1): + tsup@8.3.5(postcss@8.4.47)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.6.1): dependencies: bundle-require: 5.0.0(esbuild@0.24.0) cac: 6.7.14 @@ -7095,7 +7364,7 @@ snapshots: esbuild: 0.24.0 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.4.47)(yaml@2.6.1) + postcss-load-config: 6.0.1(postcss@8.4.47)(tsx@4.19.2)(yaml@2.6.1) resolve-from: 5.0.0 rollup: 4.28.0 source-map: 0.8.0-beta.0 @@ -7112,6 +7381,13 @@ snapshots: - tsx - yaml + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 diff --git a/src/decorators.ts b/src/decorators.ts index 682403b..aab86e1 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -7,7 +7,6 @@ import { ASSOCIATION_PROPERTIES_KEY } from "./sdk/tracing/tracing"; interface ObserveOptions { name?: string; sessionId?: string; - userId?: string; traceType?: TraceType; spanType?: 'DEFAULT' | 'LLM'; traceId?: string; @@ -40,7 +39,6 @@ export async function observe Ret { name, sessionId, - userId, traceType, spanType, traceId, @@ -53,10 +51,6 @@ export async function observe Ret if (sessionId) { associationProperties = { ...associationProperties, "session_id": sessionId }; } - if (userId) { - console.warn("`userId` is deprecated. Use `Laminar.withMetadata` to set key-value pairs."); - associationProperties = { ...associationProperties, "user_id": userId }; - } if (traceType) { associationProperties = { ...associationProperties, "trace_type": traceType }; } diff --git a/src/laminar.ts b/src/laminar.ts index 07ceb34..a173805 100644 --- a/src/laminar.ts +++ b/src/laminar.ts @@ -33,7 +33,7 @@ import { SPAN_TYPE, LaminarAttributes, } from './sdk/tracing/attributes'; -import { RandomIdGenerator } from '@opentelemetry/sdk-trace-base'; +import { RandomIdGenerator, SpanProcessor } from '@opentelemetry/sdk-trace-base'; // for docstring import { BasicTracerProvider, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; @@ -48,6 +48,7 @@ interface LaminarInitializeProps { grpcPort?: number; instrumentModules?: InitializeOptions["instrumentModules"]; useExternalTracerProvider?: boolean; + _spanProcessor?: SpanProcessor; } export class Laminar { @@ -112,6 +113,7 @@ export class Laminar { grpcPort, instrumentModules, useExternalTracerProvider, + _spanProcessor, }: LaminarInitializeProps) { let key = projectApiKey ?? process.env.LMNR_PROJECT_API_KEY; @@ -141,12 +143,20 @@ export class Laminar { metadata, }); + if (_spanProcessor) { + console.warn("Laminar using a custom span processor. This feature is " + + "added for tests only. Any use of this feature outside of tests " + + "is not supported and advised against." + ); + } + traceloopInitialize({ exporter, silenceInitializationMessage: true, instrumentModules, disableBatch: false, useExternalTracerProvider, + processor: _spanProcessor, }); } @@ -346,12 +356,12 @@ export class Laminar { const currentSpan = trace.getActiveSpan(); if (currentSpan !== undefined && isSpanContextValid(currentSpan.spanContext())) { if (sessionId) { - currentSpan.setAttribute(SESSION_ID, JSON.stringify(sessionId)); + currentSpan.setAttribute(SESSION_ID, sessionId); } } let associationProperties = {}; if (sessionId) { - associationProperties = { ...associationProperties, "session_id": JSON.stringify(sessionId) }; + associationProperties = { ...associationProperties, "session_id": sessionId }; } let entityContext = contextApi.active(); @@ -393,12 +403,12 @@ export class Laminar { const currentSpan = trace.getActiveSpan(); if (currentSpan !== undefined && isSpanContextValid(currentSpan.spanContext())) { for (const [key, value] of Object.entries(metadata)) { - currentSpan.setAttribute(`${ASSOCIATION_PROPERTIES}.metadata.${key}`, JSON.stringify(value)); + currentSpan.setAttribute(`${ASSOCIATION_PROPERTIES}.metadata.${key}`, value); } } let metadataAttributes = {}; for (const [key, value] of Object.entries(metadata)) { - metadataAttributes = { ...metadataAttributes, [`metadata.${key}`]: JSON.stringify(value) }; + metadataAttributes = { ...metadataAttributes, [`metadata.${key}`]: value }; } let entityContext = contextApi.active(); @@ -539,7 +549,7 @@ export class Laminar { ...labelProperties, } const span = getTracer().startSpan(name, { attributes }, entityContext); - if (traceId) { + if (traceId && isStringUUID(traceId)) { span.setAttribute(OVERRIDE_PARENT_SPAN, true); } if (input) { @@ -561,16 +571,28 @@ export class Laminar { * * See {@link startSpan} docs for a usage example */ - public static withSpan(span: Span, fn: () => T, endOnExit?: boolean): T { + public static withSpan(span: Span, fn: () => T, endOnExit?: boolean): T | Promise { return contextApi.with(trace.setSpan(contextApi.active(), span), () => { - try{ + try { const result = fn(); + if (result instanceof Promise) { + return result.finally(() => { + if (endOnExit !== undefined && endOnExit) { + span.end(); + } + }); + } + if (endOnExit !== undefined && endOnExit) { + span.end(); + } return result; } - finally { + catch (error) { + span.recordException(error as Error); if (endOnExit !== undefined && endOnExit) { span.end(); } + throw error; } }); } diff --git a/src/sdk/tracing/decorators.ts b/src/sdk/tracing/decorators.ts index 7ab0342..995d25c 100644 --- a/src/sdk/tracing/decorators.ts +++ b/src/sdk/tracing/decorators.ts @@ -112,7 +112,7 @@ export function withEntity< try { res = fn.apply(thisArg, args); } catch (error) { - processException(error as Error, span); + span.recordException(error as Error); span.end(); throw error; } @@ -135,7 +135,7 @@ export function withEntity< return resolvedRes; }) .catch((error) => { - processException(error as Error, span); + span.recordException(error as Error); span.end(); throw error; }); @@ -181,12 +181,3 @@ function cleanInput(input: unknown): unknown { function serialize(input: unknown): string { return JSON.stringify(cleanInput(input)); } - -function processException(error: Error, span: Span) { - span.addEvent("exception", { - "exception.type": error.name, - "exception.message": error.message, - "exception.stack": error.stack, - "exception.escaped": true, - }); -} diff --git a/src/sdk/tracing/index.ts b/src/sdk/tracing/index.ts index 5502f53..3196905 100644 --- a/src/sdk/tracing/index.ts +++ b/src/sdk/tracing/index.ts @@ -9,7 +9,7 @@ import { SPAN_PATH_KEY, } from "./tracing"; import { _configuration } from "../configuration"; -import { NodeTracerProvider, SimpleSpanProcessor, BatchSpanProcessor, BasicTracerProvider } from "@opentelemetry/sdk-trace-node"; +import { NodeTracerProvider, SimpleSpanProcessor, BatchSpanProcessor, BasicTracerProvider, SpanProcessor } from "@opentelemetry/sdk-trace-node"; import { registerInstrumentations } from "@opentelemetry/instrumentation"; import { AnthropicInstrumentation } from "@traceloop/instrumentation-anthropic"; import { OpenAIInstrumentation } from "@traceloop/instrumentation-openai"; @@ -27,7 +27,7 @@ import { ChromaDBInstrumentation } from "@traceloop/instrumentation-chromadb"; import { QdrantInstrumentation } from "@traceloop/instrumentation-qdrant"; import { ASSOCIATION_PROPERTIES, ASSOCIATION_PROPERTIES_OVERRIDES, SPAN_INSTRUMENTATION_SOURCE, SPAN_PATH } from "./attributes"; -let _spanProcessor: SimpleSpanProcessor | BatchSpanProcessor; +let _spanProcessor: SimpleSpanProcessor | BatchSpanProcessor | SpanProcessor; let openAIInstrumentation: OpenAIInstrumentation | undefined; let anthropicInstrumentation: AnthropicInstrumentation | undefined; let azureOpenAIInstrumentation: AzureOpenAIInstrumentation | undefined; @@ -225,9 +225,10 @@ export const startTracing = (options: InitializeOptions) => { url: `${options.baseUrl}/v1/traces`, headers, }); - _spanProcessor = options.disableBatch - ? new SimpleSpanProcessor(traceExporter) - : new BatchSpanProcessor(traceExporter); + _spanProcessor = options.processor ?? + (options.disableBatch + ? new SimpleSpanProcessor(traceExporter) + : new BatchSpanProcessor(traceExporter)); _spanProcessor.onStart = (span: Span) => { const spanPath = context.active().getValue(SPAN_PATH_KEY); @@ -268,8 +269,9 @@ export const startTracing = (options: InitializeOptions) => { throw new Error("The active tracer provider does not support adding a span processor"); } } else { - const provider = new NodeTracerProvider(); - provider.addSpanProcessor(_spanProcessor); + const provider = new NodeTracerProvider({ + spanProcessors: [_spanProcessor], + }); provider.register(); registerInstrumentations({ instrumentations, diff --git a/src/types.ts b/src/types.ts index ad17d47..476a0cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,11 +78,9 @@ export type SpanType = 'DEFAULT' | 'LLM' | 'PIPELINE' | 'EXECUTOR' | 'EVALUATOR' /** * Trace types to categorize traces. * They are used as association properties passed to all spans in a trace. - * - * EVENT traces are traces created by the event runner. - * They are used to mark traces created by the "online" evaluations for semantic events. + * */ -export type TraceType = 'DEFAULT' | 'EVENT' | 'EVALUATION' +export type TraceType = 'DEFAULT' | 'EVALUATION' /** diff --git a/test/initialize.test.ts b/test/initialize.test.ts new file mode 100644 index 0000000..424b444 --- /dev/null +++ b/test/initialize.test.ts @@ -0,0 +1,23 @@ +import { describe, it } from "node:test"; +import { Laminar } from "../src/index"; +import assert from "node:assert"; + +describe("initialize", () => { + it("initializes", () => { + Laminar.initialize({ + projectApiKey: "test" + }); + + assert.strictEqual(Laminar.initialized(), true); + }); + + it("throws an error if projectApiKey is not provided", () => { + assert.throws(() => Laminar.initialize({}), Error); + }); + + it("throws an error if baseUrl has ports", () => { + assert.throws(() => Laminar.initialize({ + baseUrl: "http://localhost:8080" + }), Error); + }); +}); diff --git a/test/tracing.test.ts b/test/tracing.test.ts new file mode 100644 index 0000000..11a2669 --- /dev/null +++ b/test/tracing.test.ts @@ -0,0 +1,479 @@ +import { afterEach, describe, it } from "node:test"; +import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"; +import { Laminar, LaminarAttributes, observe, TracingLevel, withLabels, withTracingLevel } from "../src/index"; +import assert from "node:assert"; + +const exporter = new InMemorySpanExporter(); +const processor = new SimpleSpanProcessor(exporter); +Laminar.initialize({ + projectApiKey: "test", + _spanProcessor: processor, +}); + +describe("tracing", () => { + afterEach(() => { + exporter.reset(); + }); + + it("observes a wrapped function", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({name: "test"}, fn, 1, 2); + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + + assert.strictEqual(spans[0].attributes['lmnr.association.properties.label.endpoint'], undefined); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("observes using withSpan", async () => { + const fn = (a: number, b: number) => a + b; + const span = Laminar.startSpan({name: "test", input: "my_input"}); + const result = Laminar.withSpan( + span, + () => { + const result = fn(1, 2); + Laminar.setSpanOutput(result); + return result; + }, + true + ); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify("my_input")); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("observes a wrapped async function", async () => { + const fn = async (a: number, b: number) => a + b; + const result = await observe({name: "test"}, fn, 1, 2); + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("observes using withSpan with async functions", async () => { + const fn = async (a: number, b: number) => a + b; + const span = Laminar.startSpan({name: "test", input: "my_input"}); + const result = await Laminar.withSpan( + span, + async () => { + const result = await fn(1, 2); + Laminar.setSpanOutput(result); + return result; + }, + true + ); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify("my_input")); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets span name to function name if not provided to observe", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({}, fn, 1, 2); + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "fn"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets span type to LLM when spanType is LLM in observe", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({name: "test", spanType: "LLM"}, fn, 1, 2); + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.type'], 'LLM'); + + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets span type to LLM when spanType is LLM in startSpan", async () => { + const span = Laminar.startSpan({name: "test", spanType: "LLM"}); + const result = Laminar.withSpan(span, () => { + return 3; + }, true); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.type'], 'LLM'); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the trace id override in observe", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({name: "test", traceId: "01234567-89ab-cdef-0123-456789abcdef"}, fn, 1, 2); + + assert.strictEqual(result, 3); + const spans = exporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].name, "test"); + + assert.strictEqual(spans[0].attributes["lmnr.internal.override_parent_span"], true); + assert.strictEqual(spans[0].spanContext().traceId, "0123456789abcdef0123456789abcdef"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the trace id override in startSpan", async () => { + const span = Laminar.startSpan({name: "test", traceId: "01234567-89ab-cdef-0123-456789abcdef"}); + const result = Laminar.withSpan(span, () => { + return 3; + }, true); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + + assert.strictEqual(spans[0].attributes["lmnr.internal.override_parent_span"], true); + assert.strictEqual(spans[0].spanContext().traceId, "0123456789abcdef0123456789abcdef"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("does not override the trace id in observe if it is not a valid uuid", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({name: "test", traceId: "not-a-uuid"}, fn, 1, 2); + + assert.strictEqual(result, 3); + const spans = exporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].name, "test"); + + assert.strictEqual(spans[0].attributes["lmnr.internal.override_parent_span"], undefined); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + assert.notEqual(spans[0].spanContext().traceId, "not-a-uuid"); + }); + + it("does not override the trace id in startSpan if it is not a valid uuid", async () => { + const span = Laminar.startSpan({name: "test", traceId: "not-a-uuid"}); + const result = Laminar.withSpan(span, () => { + return 3; + }, true); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes["lmnr.internal.override_parent_span"], undefined); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + assert.notEqual(spans[0].spanContext().traceId, "not-a-uuid"); + }); + + it("sets the session id in observe", async () => { + const fn = (a: number, b: number) => a + b; + const result = await observe({name: "test", sessionId: "123"}, fn, 1, 2); + + assert.strictEqual(result, 3); + const spans = exporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].name, "test"); + + assert.strictEqual(spans[0].attributes['lmnr.association.properties.session_id'], "123"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("observes nested functions", async () => { + const double = (a: number) => a * 2; + const fn = async (a: number, b: number) => a + await observe({name: "double"}, double, b); + const result = await observe({name: "test"}, fn, 1, 2); + + assert.strictEqual(result, 5); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const testSpan = spans.find(span => span.name === "test"); + const doubleSpan = spans.find(span => span.name === "double"); + + assert.strictEqual(testSpan?.attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(testSpan?.attributes['lmnr.span.output'], "5"); + + assert.strictEqual(doubleSpan?.attributes['lmnr.span.input'], JSON.stringify([2])); + assert.strictEqual(doubleSpan?.attributes['lmnr.span.output'], "4"); + + assert.strictEqual(doubleSpan?.parentSpanId, testSpan?.spanContext().spanId); + assert.strictEqual(testSpan?.spanContext().traceId, doubleSpan?.spanContext().traceId); + assert.strictEqual(testSpan?.attributes['lmnr.span.instrumentation_source'], "javascript"); + assert.strictEqual(doubleSpan?.attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the labels on wrapped functions", async () => { + const fn = (a: number, b: number) => a + b; + const result = await withLabels( + {endpoint: "some-endpoint"}, + async (a, b) => await observe({name: "inner"}, fn, a, b), + 1, 2 + ); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.label.endpoint'], "some-endpoint"); + + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the labels on startSpan", async () => { + const span = Laminar.startSpan({name: "test", labels: {endpoint: "some-endpoint"}}); + const result = Laminar.withSpan(span, () => { + return 3; + }, true); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.label.endpoint'], "some-endpoint"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("doesn't set the labels on the current span", async () => { + const fn = (a: number, b: number) => withLabels({endpoint: "some-endpoint"}, () => a + b); + const result = await observe({name: "test"}, fn, 1, 2); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.label.endpoint'], undefined); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the labels on nested functions", async () => { + const double = (a: number) => a * 2; + const fn = async (a: number, b: number) => a + await observe({name: "double"}, double, b); + const result = await withLabels({endpoint: "some-endpoint"}, async (a, b) => await observe({name: "test"}, fn, a, b), 1, 2); + + assert.strictEqual(result, 5); + + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + const testSpan = spans.find(span => span.name === "test"); + const doubleSpan = spans.find(span => span.name === "double"); + + assert.strictEqual(testSpan?.attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(testSpan?.attributes['lmnr.span.output'], "5"); + assert.strictEqual(testSpan?.attributes['lmnr.association.properties.label.endpoint'], "some-endpoint"); + + assert.strictEqual(doubleSpan?.attributes['lmnr.span.input'], JSON.stringify([2])); + assert.strictEqual(doubleSpan?.attributes['lmnr.span.output'], "4"); + assert.strictEqual(doubleSpan?.attributes['lmnr.association.properties.label.endpoint'], "some-endpoint"); + + assert.strictEqual(doubleSpan?.parentSpanId, testSpan?.spanContext().spanId); + assert.strictEqual(testSpan?.spanContext().traceId, doubleSpan?.spanContext().traceId); + assert.strictEqual(doubleSpan?.attributes['lmnr.span.instrumentation_source'], "javascript"); + assert.strictEqual(testSpan?.attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the tracing level attribute when withTracingLevel is used", async () => { + const fn = (a: number, b: number) => a + b; + const result = await withTracingLevel( + TracingLevel.META_ONLY, + async (a, b) => await observe({name: "span_with_meta_only"}, fn, a, b), + 1, 2 + ); + + const result2 = await withTracingLevel( + TracingLevel.OFF, + async (a, b) => await observe({name: "span_with_off"}, fn, a, b), + 1, 2 + ); + + assert.strictEqual(result, 3); + assert.strictEqual(result2, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + + const spanWithMetaOnly = spans.find(span => span.name === "span_with_meta_only"); + const spanWithOff = spans.find(span => span.name === "span_with_off"); + + assert.strictEqual(spanWithMetaOnly?.attributes['lmnr.internal.tracing_level'], "meta_only"); + assert.strictEqual(spanWithMetaOnly?.attributes['lmnr.span.instrumentation_source'], "javascript"); + + assert.strictEqual(spanWithOff?.attributes['lmnr.internal.tracing_level'], "off"); + assert.strictEqual(spanWithOff?.attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the session id attribute when withSession is used", async () => { + const fn = (a: number, b: number) => a + b; + const result = await Laminar.withSession("123", async () => await observe({name: "test"}, fn, 1, 2)); + + assert.strictEqual(result, 3); + const spans = exporter.getFinishedSpans(); + + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].name, "test"); + + assert.strictEqual(spans[0].attributes['lmnr.association.properties.session_id'], "123"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the session id on the current span when withSession is used", async () => { + const fn = (a: number, b: number) => + Laminar.withSession("123", () => a + b); + const result = await observe({name: "test"}, fn, 1, 2); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.session_id'], "123"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the metadata on the current span when withMetadata is used", async () => { + const fn = (a: number, b: number) => Laminar.withMetadata({my_metadata_key: "my_metadata_value"}, () => a + b); + const result = await observe({name: "test"}, fn, 1, 2); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.metadata.my_metadata_key'], "my_metadata_value"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the metadata when withMetadata is used", async () => { + const fn = (a: number, b: number) => a + b; + const result = await Laminar.withMetadata( + {my_metadata_key: "my_metadata_value"}, + async () => await observe({name: "test"}, fn, 1, 2), + ); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.association.properties.metadata.my_metadata_key'], "my_metadata_value"); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("sets the attributes with setSpanAttributes", async () => { + const fn = (a: number, b: number) => { + Laminar.setSpanAttributes({ + [LaminarAttributes.PROVIDER]: "openai", + [LaminarAttributes.REQUEST_MODEL]: "gpt-4o-date-version", + [LaminarAttributes.RESPONSE_MODEL]: "gpt-4o", + [LaminarAttributes.INPUT_TOKEN_COUNT]: 100, + [LaminarAttributes.OUTPUT_TOKEN_COUNT]: 200, + }); + + return a + b; + } + const result = await observe({name: "test"}, fn, 1, 2); + + assert.strictEqual(result, 3); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].attributes['lmnr.span.input'], JSON.stringify([1, 2])); + assert.strictEqual(spans[0].attributes['lmnr.span.output'], "3"); + + assert.strictEqual(spans[0].attributes['gen_ai.system'], "openai"); + assert.strictEqual(spans[0].attributes['gen_ai.request.model'], "gpt-4o-date-version"); + assert.strictEqual(spans[0].attributes['gen_ai.response.model'], "gpt-4o"); + assert.strictEqual(spans[0].attributes['gen_ai.usage.input_tokens'], 100); + assert.strictEqual(spans[0].attributes['gen_ai.usage.output_tokens'], 200); + + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + }); + + it("processes exceptions in observe", async () => { + const fn = () => { + throw new Error("test err"); + }; + await assert.rejects( + async () => await observe({name: "test"}, fn) + ); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + assert.strictEqual(spans[0].attributes['lmnr.span.instrumentation_source'], "javascript"); + + const events = spans[0].events; + assert.strictEqual(events.length, 1); + assert.strictEqual(events[0].name, "exception"); + assert.strictEqual(events[0].attributes?.['exception.type'], "Error"); + assert.strictEqual(events[0].attributes?.['exception.message'], "test err"); + assert.strictEqual(events[0].attributes?.['exception.stacktrace']?.toString().startsWith('Error: test err\n at'), true); + }); + + it("processes exceptions in withSpan", async () => { + const span = Laminar.startSpan({name: "test"}); + const fn = () => { + throw new Error("test error"); + }; + await assert.rejects( + async () => await Laminar.withSpan(span, fn, true) + ); + + const spans = exporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].name, "test"); + + const events = spans[0].events; + assert.strictEqual(events.length, 1); + assert.strictEqual(events[0].name, "exception"); + assert.strictEqual(events[0].attributes?.['exception.type'], "Error"); + assert.strictEqual(events[0].attributes?.['exception.message'], "test error"); + assert.strictEqual(events[0].attributes?.['exception.stacktrace']?.toString().startsWith('Error: test error\n at'), true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index b1b0c16..5c10782 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "ES2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "ES2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,9 +25,9 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs", /* Specify what module code is generated. */ + "module": "ESNext", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ @@ -104,5 +104,6 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src", "test"] }