diff --git a/.gitignore b/.gitignore index e96787c..568a9c1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules __screenshots__ dist *.tgz +.vitest-attachments # logs *.log diff --git a/examples/with-tracing/src/main.ts b/examples/with-tracing/src/main.ts index 21202be..e05a067 100644 --- a/examples/with-tracing/src/main.ts +++ b/examples/with-tracing/src/main.ts @@ -60,11 +60,11 @@ registerInstrumentations({ }); // --- Button handlers --- -document.getElementById('fetch-button')!.addEventListener('click', () => { +document.getElementById('fetch-button')?.addEventListener('click', () => { fetch('https://httpbin.org/get'); }); -document.getElementById('xhr-button')!.addEventListener('click', () => { +document.getElementById('xhr-button')?.addEventListener('click', () => { const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://httpbin.org/get'); xhr.send(); diff --git a/package-lock.json b/package-lock.json index 43f43e4..ef2a98e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1520,6 +1520,10 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/browser": { + "resolved": "packages/browser", + "link": true + }, "node_modules/@opentelemetry/browser-instrumentation": { "resolved": "packages/instrumentation", "link": true @@ -1558,6 +1562,180 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.214.0.tgz", + "integrity": "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-metrics": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", + "integrity": "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-transformer": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", + "integrity": "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-metrics": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", + "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", + "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-http": { "version": "0.218.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", @@ -6943,9 +7121,312 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/browser": { + "name": "@opentelemetry/browser", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "^0.213.0", + "@opentelemetry/core": "^2.6.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/sdk-logs": "^0.213.0", + "@opentelemetry/sdk-metrics": "^2.6.1", + "@opentelemetry/sdk-trace-base": "^2.6.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + } + }, + "packages/browser/node_modules/@opentelemetry/api-logs": { + "version": "0.213.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.213.0.tgz", + "integrity": "sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "packages/browser/node_modules/@opentelemetry/core": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.1.tgz", + "integrity": "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/exporter-logs-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.214.0.tgz", + "integrity": "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/sdk-logs": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "packages/browser/node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "packages/browser/node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/sdk-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", + "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.214.0.tgz", + "integrity": "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-exporter-base": "0.214.0", + "@opentelemetry/otlp-transformer": "0.214.0", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "packages/browser/node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.214.0.tgz", + "integrity": "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/otlp-transformer": "0.214.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "packages/browser/node_modules/@opentelemetry/otlp-transformer": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.214.0.tgz", + "integrity": "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/sdk-logs": "0.214.0", + "@opentelemetry/sdk-metrics": "2.6.1", + "@opentelemetry/sdk-trace-base": "2.6.1", + "protobufjs": "^7.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "packages/browser/node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", + "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "packages/browser/node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-logs": { + "version": "0.214.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", + "integrity": "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/sdk-logs": { + "version": "0.213.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.213.0.tgz", + "integrity": "sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.213.0", + "@opentelemetry/core": "2.6.0", + "@opentelemetry/resources": "2.6.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", + "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", + "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "packages/browser/node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", + "integrity": "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "packages/browser/node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "packages/instrumentation": { "name": "@opentelemetry/browser-instrumentation", - "version": "0.5.1", + "version": "0.5.2", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "^0.218.0", diff --git a/package.json b/package.json index 2416004..a9fd673 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,11 @@ "peerDependencies": { "typescript": "^6.0.3" }, + "overrides": { + "@arethetypeswrong/core": { + "fflate": "0.8.2" + } + }, "workspaces": [ "examples/*", "packages/*", diff --git a/packages/browser/CHANGELOG.md b/packages/browser/CHANGELOG.md new file mode 100644 index 0000000..d43cac1 --- /dev/null +++ b/packages/browser/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +### Features + +### Bug Fixes diff --git a/packages/browser/LICENSE b/packages/browser/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/packages/browser/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/browser/README.md b/packages/browser/README.md new file mode 100644 index 0000000..a1a71e1 --- /dev/null +++ b/packages/browser/README.md @@ -0,0 +1,110 @@ +# @opentelemetry/browser + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +Browser-oriented helpers to start OpenTelemetry **logs** and **traces** with OTLP HTTP export. Configuration is split by signal so you import only what you use, which helps bundlers tree-shake unused code. A small **`combineSdks`** helper merges multiple signal starters into one `start`/`shutdown` object with shared global options (endpoint, headers, resource). + +## Installation + +```bash +npm install @opentelemetry/browser @opentelemetry/api +``` + +`@opentelemetry/api` is a **peer dependency** (v1.9+). Install it alongside this package. + +## Package layout (subpath exports) + +The package does not rely on a single heavy entry point. Use explicit subpaths: + +| Subpath | Purpose | +| ------------------------------- | -------------------------------------------- | +| `@opentelemetry/browser/logs` | `startLogsSdk` — logs pipeline + OTLP export | +| `@opentelemetry/browser/traces` | `startTracesSdk` — traces + OTLP export | +| `@opentelemetry/browser/sdk` | `startBrowserSdk` — both signals composed | + +## Core types + +Configuration interfaces are defined in the implementation (`GlobalConfig`, `LogsConfig`, `TracesConfig`). Highlights: + +- **`GlobalConfig`** (for `startBrowserSdk` only): shared OTLP export configuration (base `url`, default `http://localhost:4318`), optional `resource`, plus fields reserved for future use (e.g. diagnostics, limits). +- **`LogsConfig`**: `resource`, OTLP export configuration (`url`, default `http://localhost:4318/v1/logs`), batch processor tuning `logRecordLimits`. +- **`TracesConfig`**: optional `contextManager` and `textMapPropagator` (see `@opentelemetry/api`), `resource`, `sampler`, `spanLimits`, OTLP export configuration (`url`, default `http://localhost:4318/v1/traces`), batch span processor tuning . + +## Usage + +### Logs only + +```ts +import { startLogsSdk } from '@opentelemetry/browser/logs'; + +const logsSdk = startLogsSdk({ + // e.g. exportConfig, resource, logRecordLimits, … +}); + +// when tearing down (e.g. page unload) +await logsSdk.shutdown(); +``` + +`startLogsSdk` registers a global logger provider (`@opentelemetry/api-logs`) with a batch processor and OTLP HTTP exporter. + +### Traces only + +```ts +import { startTracesSdk } from '@opentelemetry/browser/traces'; + +const tracesSdk = startTracesSdk({ + // e.g. exportConfig, sampler, contextManager, textMapPropagator, … +}); + +await tracesSdk.shutdown(); +``` + +`startTracesSdk` registers a global tracer provider and optional context manager / propagator when provided. + +### Multiple signals with shared settings + +Use **`startBrowserSdk`** from `@opentelemetry/browser/sdk` to pass one merged config (global options plus nested `logs` / `traces` sections) and get a single `shutdown`: + +```ts +import { startBrowserSdk } from '@opentelemetry/browser/sdk'; + +const mySdk = startBrowserSdk({ + exportConfg: { + url: 'https://otel.example.com:4318', + headers: { Authorization: 'Bearer …' }, + }, + logs: { + exportConfig: { + headers: { 'x-logs': 'foo' }, + }, + }, + traces: { + exportConfig: { + headers: { 'x-traces': 'bar' }, + }, + }, +}); + +await mySdk.shutdown(); +``` + +Behavior notes: + +- If you omit signal-specific OTLP export configuration, the global export URL is used as a base and paths `/v1/logs` and `/v1/traces` are applied automatically. +- **`headers`** at the global level is applied to each signal unless overridden by signal specific ones. +- A default **`resource`** is applied when using `startBrowserSdk` if you do not set `resource` globally or per signal. + +## After startup + +Use the standard OpenTelemetry APIs for your signals, for example `@opentelemetry/api` for traces and `@opentelemetry/api-logs` for logs, now that global providers are registered. + +## License + +Apache 2.0 — see [LICENSE][license-url]. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-browser/discussions/landing +[license-url]: https://github.com/open-telemetry/opentelemetry-browser/blob/main/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[npm-url]: https://www.npmjs.com/package/@opentelemetry/browser +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fbrowser.svg diff --git a/packages/browser/package.json b/packages/browser/package.json new file mode 100644 index 0000000..89d134f --- /dev/null +++ b/packages/browser/package.json @@ -0,0 +1,55 @@ +{ + "name": "@opentelemetry/browser", + "version": "0.1.0", + "description": "OpenTelemetry browser main package.", + "keywords": [ + "opentelemetry", + "browser", + "web", + "sdk" + ], + "homepage": "https://github.com/open-telemetry/opentelemetry-browser", + "bugs": "https://github.com/open-telemetry/opentelemetry-browser/issues", + "license": "Apache-2.0", + "author": "OpenTelemetry Authors", + "repository": { + "type": "git", + "url": "git+https://github.com/open-telemetry/opentelemetry-browser.git", + "directory": "packages/browser" + }, + "type": "module", + "exports": { + "./propagation": "./dist/propagation.js", + "./logs": "./dist/logs.js", + "./traces": "./dist/traces.js", + "./sdk": "./dist/sdk.js" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "watch": "tsdown --watch --no-clean", + "check-types": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest --coverage" + }, + "dependencies": { + "@opentelemetry/api-logs": "^0.213.0", + "@opentelemetry/core": "^2.6.0", + "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/sdk-logs": "^0.213.0", + "@opentelemetry/sdk-metrics": "^2.6.1", + "@opentelemetry/sdk-trace-base": "^2.6.1" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/browser/src/context.test.ts b/packages/browser/src/context.test.ts new file mode 100644 index 0000000..01b2c3d --- /dev/null +++ b/packages/browser/src/context.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Context, ContextManager } from '@opentelemetry/api'; +import { createContextKey, ROOT_CONTEXT } from '@opentelemetry/api'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getDefaultContextManager } from './context.ts'; + +describe('Default ContextManager', () => { + const key1 = createContextKey('test key 1'); + let contextManager: ContextManager; + + beforeEach(() => { + contextManager = getDefaultContextManager(); + contextManager.enable(); + }); + + afterEach(() => { + contextManager.disable(); + }); + + describe('.enable()', () => { + it('should work', () => { + expect(contextManager.enable()).toBe(contextManager); + expect(contextManager.active()).toBe(ROOT_CONTEXT); + }); + }); + + describe('.disable()', () => { + it('should work', () => { + expect(contextManager.disable()).toBe(contextManager); + expect(contextManager.active()).toBe(ROOT_CONTEXT); + }); + }); + + describe('.with()', () => { + it('should run the callback (null as context)', () => { + return new Promise((done) => { + contextManager.with( + null as unknown as Context, + done as unknown as () => void, + ); + }); + }); + + it('should run the callback (object as target)', () => { + const test = ROOT_CONTEXT.setValue(key1, 1); + + return new Promise((done) => { + contextManager.with(test, () => { + expect(contextManager.active()).toBe(test); + return done(null); + }); + }); + }); + + it('should run the callback (when disabled)', () => { + contextManager.disable(); + return new Promise((done) => { + contextManager.with(null as unknown as Context, () => { + contextManager.enable(); + return done(null); + }); + }); + }); + + it('should rethrow errors', () => { + expect(() => { + contextManager.with(contextManager.active(), () => { + throw new Error('This should be rethrown'); + }); + }).throws(); + }); + + it('should stack and restore context', () => { + const ctx1 = ROOT_CONTEXT.setValue(key1, 'ctx1'); + const ctx2 = ROOT_CONTEXT.setValue(key1, 'ctx2'); + const ctx3 = ROOT_CONTEXT.setValue(key1, 'ctx3'); + contextManager.with(ctx1, () => { + expect(contextManager.active()).toBe(ctx1); + contextManager.with(ctx2, () => { + expect(contextManager.active()).toBe(ctx2); + contextManager.with(ctx3, () => { + expect(contextManager.active()).toBe(ctx3); + }); + expect(contextManager.active()).toBe(ctx2); + }); + expect(contextManager.active()).toBe(ctx1); + }); + expect(contextManager.active()).toBe(ROOT_CONTEXT); + }); + + it('should forward this, arguments and return value', () => { + function fnWithThis(this: string, a: string, b: number): string { + expect(this).toEqual('that'); + expect(a).toEqual('one'); + expect(b).toEqual(2); + return 'done'; + } + + const res = contextManager.with( + ROOT_CONTEXT, + fnWithThis, + 'that', + 'one', + 2, + ); + expect(res).toEqual('done'); + expect(contextManager.with(ROOT_CONTEXT, () => 3.14)).toEqual(3.14); + }); + }); +}); diff --git a/packages/browser/src/context.ts b/packages/browser/src/context.ts new file mode 100644 index 0000000..c47b445 --- /dev/null +++ b/packages/browser/src/context.ts @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Context, ContextManager } from '@opentelemetry/api'; +import { ROOT_CONTEXT } from '@opentelemetry/api'; + +/** + * Returns a simple context manager which stacks the parent context + * when a function is being called. + * @returns {ContextManager} + */ +export function getDefaultContextManager(): ContextManager { + let _currentContext = ROOT_CONTEXT; + let _enabled = false; + + return { + active: (): Context => _currentContext, + with: ReturnType>( + context: Context, + fn: F, + thisArg?: ThisParameterType, + ...args: A + ): ReturnType => { + const previousContext = _currentContext; + _currentContext = context || ROOT_CONTEXT; + try { + return fn.call(thisArg, ...args); + } finally { + _currentContext = previousContext; + } + }, + bind: function (context: Context, target: T): T { + // if no specific context to propagate is given, we use the current one + if (context === undefined) { + context = this.active(); + } + if (typeof target === 'function') { + const manager = this; + const contextWrapper = function (this: unknown, ...args: unknown[]) { + return manager.with(context, () => target.apply(this, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + return contextWrapper as unknown as T; + } + return target; + }, + enable: function (): ContextManager { + if (!_enabled) { + _currentContext = ROOT_CONTEXT; + _enabled = true; + } + return this; + }, + disable: function (): ContextManager { + _currentContext = ROOT_CONTEXT; + _enabled = false; + return this; + }, + }; +} diff --git a/packages/browser/src/logs.test.ts b/packages/browser/src/logs.test.ts new file mode 100644 index 0000000..64c7872 --- /dev/null +++ b/packages/browser/src/logs.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { LoggerProvider } from '@opentelemetry/api-logs'; +import { logs } from '@opentelemetry/api-logs'; +import { BatchLogRecordProcessor } from '@opentelemetry/sdk-logs'; +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { startLogsSdk } from './logs.ts'; +import type { WebSdk } from './types.ts'; + +const BLRP_SCHEDULE_DELAY = 10; + +describe('startLogsSdk', () => { + const response = { ok: true, json: async () => ({ ok: true }) } as Response; + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(response); + const setGlobalLoggerProviderSpy = vi + .spyOn(logs, 'setGlobalLoggerProvider') + .mockImplementation((p) => { + loggerProvider = p; + return p; + }); + const getLoggerProviderSpy = vi + .spyOn(logs, 'getLoggerProvider') + .mockImplementation(() => loggerProvider); + let loggerProvider: LoggerProvider; + let logsSdk: WebSdk; + + // NOTE: we mock the registration of the logger provider because + // the logs API only allow to register once. With the mock we can use + // a dedicated provider for the test + afterAll(() => { + setGlobalLoggerProviderSpy.mockRestore(); + getLoggerProviderSpy.mockRestore(); + fetchSpy.mockRestore(); + }); + beforeEach(async () => { + setGlobalLoggerProviderSpy.mockClear(); + getLoggerProviderSpy.mockClear(); + fetchSpy.mockClear(); + await logsSdk?.shutdown(); + }); + + it('should register a LoggerProvider with a BatchLogRecordProcessor', async () => { + // Act + logsSdk = startLogsSdk(); + + // Assert + expect(setGlobalLoggerProviderSpy).callCount(1); + // @ts-expect-error -- accessing private properties + const processors = logs.getLoggerProvider()['_sharedState']['processors']; + expect(processors.length).toBe(1); + expect(processors[0]).toBeInstanceOf(BatchLogRecordProcessor); + }); + + it('should use the default configuration for exporters', async () => { + // Act + logsSdk = startLogsSdk({ + processorConfig: { + // NOTE: we set a short delay to speed up tests and avoid test timeouts + scheduledDelayMillis: BLRP_SCHEDULE_DELAY, + }, + }); + logs.getLogger('logs-sdk-test').emit({ eventName: 'test' }); + await new Promise((r) => setTimeout(r, BLRP_SCHEDULE_DELAY + 5)); + + // Assert + expect(fetchSpy).toHaveBeenCalledOnce(); + expect(fetchSpy.mock.lastCall?.[0]).toEqual( + 'http://localhost:4318/v1/logs', + ); + expect(fetchSpy.mock.lastCall?.[1]).containSubset({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + it('should accept signal specific OTLP endpoint and headers', async () => { + // Act + logsSdk = startLogsSdk({ + processorConfig: { + // NOTE: we set a short delay to speed up tests and avoid test timeouts + scheduledDelayMillis: BLRP_SCHEDULE_DELAY, + }, + exportConfig: { + url: 'http://otlp-signal-endpoint:4318/v1/logs', + headers: { bar: 'baz' }, + }, + }); + logs.getLogger('logs-sdk-test').emit({ eventName: 'test' }); + await new Promise((r) => setTimeout(r, BLRP_SCHEDULE_DELAY + 5)); + + // Assert + expect(setGlobalLoggerProviderSpy).callCount(1); + expect(fetchSpy).toHaveBeenCalledOnce(); + expect(fetchSpy.mock.lastCall?.[0]).toEqual( + 'http://otlp-signal-endpoint:4318/v1/logs', + ); + expect(fetchSpy.mock.lastCall?.[1]).containSubset({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + bar: 'baz', + }, + }); + }); +}); diff --git a/packages/browser/src/logs.ts b/packages/browser/src/logs.ts new file mode 100644 index 0000000..09bec59 --- /dev/null +++ b/packages/browser/src/logs.ts @@ -0,0 +1,43 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { logs } from '@opentelemetry/api-logs'; +import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http'; +import { + BatchLogRecordProcessor, + LoggerProvider, +} from '@opentelemetry/sdk-logs'; + +import type { LogsConfig, WebSdk } from './types.ts'; + +const DEFAULT_LOGS_OTLP_ENDPOINT = 'http://localhost:4318/v1/logs'; + +/** + * @param config The configuration for logs + * @returns {WebSdk} + */ +export function startLogsSdk(config?: LogsConfig): WebSdk { + const logsEndpoint = config?.exportConfig?.url || DEFAULT_LOGS_OTLP_ENDPOINT; + + const logsProcessor = new BatchLogRecordProcessor( + new OTLPLogExporter({ + url: logsEndpoint, + headers: config?.exportConfig?.headers, + }), + config?.processorConfig, + ); + const loggerProvider = new LoggerProvider({ + resource: config?.resource, + logRecordLimits: config?.logRecordLimits, + processors: [logsProcessor], + }); + logs.setGlobalLoggerProvider(loggerProvider); + + return { + shutdown() { + return loggerProvider.shutdown(); + }, + }; +} diff --git a/packages/browser/src/propagation.ts b/packages/browser/src/propagation.ts new file mode 100644 index 0000000..1af0921 --- /dev/null +++ b/packages/browser/src/propagation.ts @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { TextMapPropagator } from '@opentelemetry/api'; +import { + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from '@opentelemetry/core'; + +/** + * Returns the default propagators: + * - W3CTraceContextPropagator + * - W3CBaggagePropagator + * @returns {TextMapPropagator} + */ +export function getDefaultPropagators(): TextMapPropagator[] { + return [new W3CTraceContextPropagator(), new W3CBaggagePropagator()]; +} diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts new file mode 100644 index 0000000..62f25f0 --- /dev/null +++ b/packages/browser/src/sdk.ts @@ -0,0 +1,111 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defaultResource } from '@opentelemetry/resources'; +import { startLogsSdk } from './logs.ts'; +import { startTracesSdk } from './traces.ts'; +import type { + GlobalConfig, + LogsConfig, + TracesConfig, + WebSdk, + WebSdkFactory, +} from './types.ts'; + +interface SdkFactories { + logs?: WebSdkFactory; + traces?: WebSdkFactory; +} + +type ConfigsFor = Partial<{ + [K in keyof T]: T[K] extends WebSdkFactory ? C : never; +}>; + +const DEFAULT_OTLP_ENDOINT = 'http://localhost:4318'; + +/** + * Combines different SDK factory functions into a single one + * which accepts a global configuration along + */ +function combineSdks( + factories: T, +): WebSdkFactory> { + // The returned function will transform some of the global + // configuration options to signal specific ones if the SDK is available + return function startSdk(config?: GlobalConfig & ConfigsFor) { + // Check the global config and set defaults + const globalConfig = (config || {}) as GlobalConfig; + + // Export + globalConfig.exportConfig = Object.assign( + { endpoint: DEFAULT_OTLP_ENDOINT }, + globalConfig.exportConfig, + ); + + // TODO: accept resource detectors? + globalConfig.resource ??= defaultResource(); + + const sdks: WebSdk[] = []; + const endpointUrl = new URL(globalConfig.exportConfig!.url!); + + // Start logs + if (factories.logs) { + const logsConfig = (config?.logs || {}) as LogsConfig; + const isGenericEndpoint = !logsConfig.exportConfig?.url; + + // Merge export configs + logsConfig.exportConfig = Object.assign( + {}, + globalConfig.exportConfig, + logsConfig.exportConfig, + ); + // Set the path if endpoint comes from general config + if (isGenericEndpoint) { + endpointUrl.pathname = '/v1/logs'; + logsConfig.exportConfig.url = endpointUrl.href; + } + logsConfig.resource ??= globalConfig.resource; + sdks.push(factories.logs(logsConfig)); + } + + // Start traces + if (factories.traces) { + const tracesConfig = (config?.traces || {}) as TracesConfig; + const isGenericEndpoint = !tracesConfig.exportConfig?.url; + + // Merge export configs + tracesConfig.exportConfig = Object.assign( + {}, + globalConfig.exportConfig, + tracesConfig.exportConfig, + ); + // Set the path if endpoint comes from general config + if (isGenericEndpoint) { + endpointUrl.pathname = '/v1/traces'; + tracesConfig.exportConfig.url = endpointUrl.href; + } + tracesConfig.resource ??= globalConfig.resource; + sdks.push(factories.traces(tracesConfig)); + } + + return { + shutdown() { + return Promise.allSettled(sdks.map((s) => s.shutdown())).then( + () => undefined, + ); + }, + }; + }; +} + +/** + * Combination of all singal SDKs into one. A shorthand for users to + * start with all signals allowing them to pass some global configuration + * options. + */ +export const startBrowserSdk = combineSdks({ + logs: startLogsSdk, + traces: startTracesSdk, +}); diff --git a/packages/browser/src/traces.test.ts b/packages/browser/src/traces.test.ts new file mode 100644 index 0000000..8e00ce2 --- /dev/null +++ b/packages/browser/src/traces.test.ts @@ -0,0 +1,114 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { TracerProvider } from '@opentelemetry/api'; +import { trace } from '@opentelemetry/api'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { startTracesSdk } from './traces.ts'; +import type { WebSdk } from './types.ts'; + +const BSP_SCHEDULE_DELAY = 10; + +describe('startTracesSdk', () => { + const response = { ok: true, json: async () => ({ ok: true }) } as Response; + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(response); + const setGlobalTracerProviderSpy = vi + .spyOn(trace, 'setGlobalTracerProvider') + .mockImplementation((p) => { + tracerProvider = p; + return true; + }); + const getLoggerProviderSpy = vi + .spyOn(trace, 'getTracerProvider') + .mockImplementation(() => tracerProvider); + let tracerProvider: TracerProvider; + let tracesSdk: WebSdk; + + // NOTE: we mock the registration of the tracer provider because + // the trace API only allow to register once. With the mock we can use + // a dedicated provider for the test + afterAll(() => { + setGlobalTracerProviderSpy.mockRestore(); + getLoggerProviderSpy.mockRestore(); + fetchSpy.mockRestore(); + }); + beforeEach(async () => { + setGlobalTracerProviderSpy.mockClear(); + getLoggerProviderSpy.mockClear(); + fetchSpy.mockClear(); + await tracesSdk?.shutdown(); + }); + + it('should register a TracerProvider with a BatchSpanProcessor', async () => { + // Act + tracesSdk = startTracesSdk(); + + // Assert + expect(setGlobalTracerProviderSpy).callCount(1); + const key = '_activeSpanProcessor'; + const subkey = '_spanProcessors'; + // @ts-expect-error -- accessing private properties + const processors = trace.getTracerProvider()[key][subkey]; + expect(processors.length).toBe(1); + expect(processors[0]).toBeInstanceOf(BatchSpanProcessor); + }); + + it('should use the default configuration for exporters', async () => { + // Act + tracesSdk = startTracesSdk({ + processorConfig: { + // NOTE: we set a short delay to speed up tests and avoid test timeouts + scheduledDelayMillis: BSP_SCHEDULE_DELAY, + }, + }); + + trace.getTracer('traces-sdk-test').startSpan('test').end(); + await new Promise((r) => setTimeout(r, BSP_SCHEDULE_DELAY + 5)); + + // Assert + expect(fetchSpy).toHaveBeenCalledOnce(); + expect(fetchSpy.mock.lastCall?.[0]).toEqual( + 'http://localhost:4318/v1/traces', + ); + expect(fetchSpy.mock.lastCall?.[1]).containSubset({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + it('should accept signal specific OTLP endpoint and headers', async () => { + // Act + tracesSdk = startTracesSdk({ + processorConfig: { + // NOTE: we set a short delay to speed up tests and avoid test timeouts + scheduledDelayMillis: BSP_SCHEDULE_DELAY, + }, + exportConfig: { + url: 'http://otlp-signal-endpoint:4318/v1/traces', + headers: { bar: 'baz' }, + }, + }); + trace.getTracer('traces-sdk-test').startSpan('test').end(); + await new Promise((r) => setTimeout(r, BSP_SCHEDULE_DELAY + 5)); + + // Assert + expect(setGlobalTracerProviderSpy).callCount(1); + expect(fetchSpy).toHaveBeenCalledOnce(); + expect(fetchSpy.mock.lastCall?.[0]).toEqual( + 'http://otlp-signal-endpoint:4318/v1/traces', + ); + expect(fetchSpy.mock.lastCall?.[1]).containSubset({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + bar: 'baz', + }, + }); + }); +}); diff --git a/packages/browser/src/traces.ts b/packages/browser/src/traces.ts new file mode 100644 index 0000000..569a6f6 --- /dev/null +++ b/packages/browser/src/traces.ts @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { context, propagation, trace } from '@opentelemetry/api'; +import { CompositePropagator } from '@opentelemetry/core'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { + BasicTracerProvider, + BatchSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; + +import type { TracesConfig, WebSdk } from './types.ts'; + +const DEFAULT_TRACES_OTLP_ENDOINT = 'http://localhost:4318/v1/traces'; + +export function startTracesSdk(config?: TracesConfig): WebSdk { + const tracesEndpoint = + config?.exportConfig?.url || DEFAULT_TRACES_OTLP_ENDOINT; + + const spanProcessor = new BatchSpanProcessor( + new OTLPTraceExporter({ + url: tracesEndpoint, + headers: config?.exportConfig?.headers, + }), + config?.processorConfig, + ); + + const tracerProvider = new BasicTracerProvider({ + // sampler: new TraceIdRatioBasedSampler( + // typeof config?.sampleRate === "number" ? config?.sampleRate : 1, + // ), + resource: config?.resource, + spanLimits: config?.spanLimits, + spanProcessors: [spanProcessor], + }); + trace.setGlobalTracerProvider(tracerProvider); + + if (config?.propagators) { + const { propagators } = config; + propagation.setGlobalPropagator(new CompositePropagator({ propagators })); + } + + if (config?.contextManager) { + context.setGlobalContextManager(config.contextManager); + } + + return { + shutdown() { + return tracerProvider.shutdown(); + }, + }; +} diff --git a/packages/browser/src/types.ts b/packages/browser/src/types.ts new file mode 100644 index 0000000..04a89d6 --- /dev/null +++ b/packages/browser/src/types.ts @@ -0,0 +1,91 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { + ContextManager, + DiagLogLevel, + TextMapPropagator, +} from '@opentelemetry/api'; +import type { Resource } from '@opentelemetry/resources'; +import type { LogRecordLimits } from '@opentelemetry/sdk-logs'; +import type { + GeneralLimits, + Sampler, + SpanLimits, +} from '@opentelemetry/sdk-trace-base'; + +/** + * Export configuration. Can be used globally or per signal + */ +export interface ExportConfig { + url?: string; + headers?: Record; +} + +/** + * Batch processor configuration. Can be used globally or per signal + */ +export interface ProcessorConfig { + scheduledDelayMillis?: number; + exportTimeoutMillis?: number; + maxQueueSize?: number; + maxExportBatchSize?: number; +} + +/** + * The global configuration of the SDK. This type is enhanced + * by the `combineSdks` function by adding a key for each + * signal used (logs, traces). Do not add a "logs" or "traces" key + * here to avoid type collision. + */ +export interface GlobalConfig { + disabled?: boolean; + logLevel?: DiagLogLevel; + // Resource & Entities related + serviceName?: string; + serviceVersion?: string; + resource?: Resource; + // Export + exportConfig?: ExportConfig; + // General Limits + generalLimits: GeneralLimits; + + // Basic options that could translate to more complex ones + // in specific signals like + // 1. `sampleRate` becomes a TraceIdRatioBasedSampler for traces + // and maybe somethign else for other signals??? (sampling logs?) + // sampleRate?: number; +} +export interface LogsConfig { + // Resource & Entities related + resource?: Resource; + // Processor + processorConfig?: ProcessorConfig; + // Export + exportConfig?: ExportConfig; + // Limits + logRecordLimits?: LogRecordLimits; +} + +export interface TracesConfig { + // Context and Propagation + contextManager?: ContextManager; + propagators?: TextMapPropagator[]; + // Resource & Entities related + resource?: Resource; + // Sampler + sampler?: Sampler; + // Processor + processorConfig?: ProcessorConfig; + // Export + exportConfig?: ExportConfig; + // Limits + spanLimits?: SpanLimits; +} + +export interface WebSdk { + shutdown(): Promise; +} +export type WebSdkFactory = (config?: T) => WebSdk; diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json new file mode 100644 index 0000000..0ff2110 --- /dev/null +++ b/packages/browser/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/browser/tsdown.config.ts b/packages/browser/tsdown.config.ts new file mode 100644 index 0000000..82b6103 --- /dev/null +++ b/packages/browser/tsdown.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsdown'; +import baseConfig from '../../tsdown.config.ts'; + +export default defineConfig({ + ...baseConfig, + entry: ['src/*.ts', '!src/*.test.ts'], +}); diff --git a/packages/browser/vitest.config.ts b/packages/browser/vitest.config.ts new file mode 100644 index 0000000..4c442c6 --- /dev/null +++ b/packages/browser/vitest.config.ts @@ -0,0 +1,21 @@ +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + projects: [ + { + test: { + name: 'browser', + include: ['src/**/*.test.ts'], + browser: { + provider: playwright(), + enabled: true, + headless: true, + instances: [{ browser: 'chromium' }], + }, + }, + }, + ], + }, +});