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"]
}