diff --git a/CHANGELOG.md b/CHANGELOG.md index 30abc26e733c..58c1bf71dce9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,104 @@ ## Unreleased -- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- feat(browser): Add environment variable support for Spotlight configuration ([#18198](https://github.com/getsentry/sentry-javascript/pull/18198)) + - `SENTRY_SPOTLIGHT`, `PUBLIC_SENTRY_SPOTLIGHT`, `NEXT_PUBLIC_SENTRY_SPOTLIGHT`, `VITE_SENTRY_SPOTLIGHT`, `NUXT_PUBLIC_SENTRY_SPOTLIGHT`, `REACT_APP_SENTRY_SPOTLIGHT`, `VUE_APP_SENTRY_SPOTLIGHT`, and `GATSBY_SENTRY_SPOTLIGHT` -- fix(node): Fix Spotlight configuration precedence to match specification (#18195) +## 10.26.0 + +### Important Changes + +- **feat(core): Instrument LangGraph Agent ([#18114](https://github.com/getsentry/sentry-javascript/pull/18114))** + +Adds support for instrumenting LangGraph StateGraph operations in Node. The LangGraph integration can be configured as follows: + +```js +Sentry.init({ + dsn: '__DSN__', + sendDefaultPii: false, // Even with PII disabled globally + integrations: [ + Sentry.langGraphIntegration({ + recordInputs: true, // Force recording input messages + recordOutputs: true, // Force recording response text + }), + ], +}); +``` + +- **feat(cloudflare/vercel-edge): Add manual instrumentation for LangGraph ([#18112](https://github.com/getsentry/sentry-javascript/pull/18112))** + +Instrumentation for LangGraph in Cloudflare Workers and Vercel Edge environments is supported by manually calling `instrumentLangGraph`: + +```js +import * as Sentry from '@sentry/cloudflare'; // or '@sentry/vercel-edge' +import { StateGraph, START, END, MessagesAnnotation } from '@langchain/langgraph'; + +// Create and instrument the graph +const graph = new StateGraph(MessagesAnnotation) + .addNode('agent', agentFn) + .addEdge(START, 'agent') + .addEdge('agent', END); + +Sentry.instrumentLangGraph(graph, { + recordInputs: true, + recordOutputs: true, +}); + +const compiled = graph.compile({ name: 'weather_assistant' }); + +await compiled.invoke({ + messages: [{ role: 'user', content: 'What is the weather in SF?' }], +}); +``` + +- **feat(node): Add OpenAI SDK v6 support ([#18244](https://github.com/getsentry/sentry-javascript/pull/18244))** + +### Other Changes + +- feat(core): Support OpenAI embeddings API ([#18224](https://github.com/getsentry/sentry-javascript/pull/18224)) +- feat(browser-utils): bump web-vitals to 5.1.0 ([#18091](https://github.com/getsentry/sentry-javascript/pull/18091)) +- feat(core): Support truncation for LangChain integration request messages ([#18157](https://github.com/getsentry/sentry-javascript/pull/18157)) +- feat(metrics): Add default `server.address` attribute on server runtimes ([#18242](https://github.com/getsentry/sentry-javascript/pull/18242)) +- feat(nextjs): Add URL to server-side transaction events ([#18230](https://github.com/getsentry/sentry-javascript/pull/18230)) +- feat(node-core): Add mechanism to prevent wrapping ai providers multiple times([#17972](https://github.com/getsentry/sentry-javascript/pull/17972)) +- feat(replay): Bump limit for minReplayDuration ([#18190](https://github.com/getsentry/sentry-javascript/pull/18190)) +- fix(browser): Add `ok` status to successful `idleSpan`s ([#18139](https://github.com/getsentry/sentry-javascript/pull/18139)) +- fix(core): Check `fetch` support with data URL ([#18225](https://github.com/getsentry/sentry-javascript/pull/18225)) +- fix(core): Decrease number of Sentry stack frames for messages from `captureConsoleIntegration` ([#18096](https://github.com/getsentry/sentry-javascript/pull/18096)) +- fix(core): Emit processed metric ([#18222](https://github.com/getsentry/sentry-javascript/pull/18222)) +- fix(core): Ensure logs past `MAX_LOG_BUFFER_SIZE` are not swallowed ([#18207](https://github.com/getsentry/sentry-javascript/pull/18207)) +- fix(core): Ensure metrics past `MAX_METRIC_BUFFER_SIZE` are not swallowed ([#18212](https://github.com/getsentry/sentry-javascript/pull/18212)) +- fix(core): Fix logs and metrics flush timeout starvation with continuous logging ([#18211](https://github.com/getsentry/sentry-javascript/pull/18211)) +- fix(core): Flatten gen_ai.request.available_tools in google-genai ([#18194](https://github.com/getsentry/sentry-javascript/pull/18194)) +- fix(core): Stringify available tools sent from vercelai ([#18197](https://github.com/getsentry/sentry-javascript/pull/18197)) +- fix(core/vue): Detect and skip normalizing Vue `VNode` objects with high `normalizeDepth` ([#18206](https://github.com/getsentry/sentry-javascript/pull/18206)) +- fix(nextjs): Avoid wrapping middleware files when in standalone mode ([#18172](https://github.com/getsentry/sentry-javascript/pull/18172)) +- fix(nextjs): Drop meta trace tags if rendered page is ISR ([#18192](https://github.com/getsentry/sentry-javascript/pull/18192)) +- fix(nextjs): Respect PORT variable for dev error symbolication ([#18227](https://github.com/getsentry/sentry-javascript/pull/18227)) +- fix(nextjs): use LRU map instead of map for ISR route cache ([#18234](https://github.com/getsentry/sentry-javascript/pull/18234)) +- fix(node): `tracingChannel` export missing in older node versions ([#18191](https://github.com/getsentry/sentry-javascript/pull/18191)) +- fix(node): Fix Spotlight configuration precedence to match specification ([#18195](https://github.com/getsentry/sentry-javascript/pull/18195)) +- fix(react): Prevent navigation span leaks for consecutive navigations ([#18098](https://github.com/getsentry/sentry-javascript/pull/18098)) +- ref(react-router): Deprecate ErrorBoundary exports ([#18208](https://github.com/getsentry/sentry-javascript/pull/18208)) + +
+ Internal Changes + +- chore: Fix missing changelog quote we use for attribution placement ([#18237](https://github.com/getsentry/sentry-javascript/pull/18237)) +- chore: move tip about prioritizing issues ([#18071](https://github.com/getsentry/sentry-javascript/pull/18071)) +- chore(e2e): Pin `@embroider/addon-shim` to 1.10.0 for the e2e ember-embroider ([#18173](https://github.com/getsentry/sentry-javascript/pull/18173)) +- chore(react-router): Fix casing on deprecation notices ([#18221](https://github.com/getsentry/sentry-javascript/pull/18221)) +- chore(test): Use correct `testTimeout` field in bundler-tests vitest config +- chore(e2e): Bump zod in e2e tests ([#18251](https://github.com/getsentry/sentry-javascript/pull/18251)) +- test(browser-integration): Fix incorrect tag value assertions ([#18162](https://github.com/getsentry/sentry-javascript/pull/18162)) +- test(profiling): Add test utils to validate Profile Chunk envelope ([#18170](https://github.com/getsentry/sentry-javascript/pull/18170)) +- ref(e2e-ember): Remove `@embroider/addon-shim` override ([#18180](https://github.com/getsentry/sentry-javascript/pull/18180)) +- ref(browser): Move trace lifecycle listeners to class function ([#18231](https://github.com/getsentry/sentry-javascript/pull/18231)) +- ref(browserprofiling): Move and rename profiler class to UIProfiler ([#18187](https://github.com/getsentry/sentry-javascript/pull/18187)) +- ref(core): Move ai integrations from utils to tracing ([#18185](https://github.com/getsentry/sentry-javascript/pull/18185)) +- ref(core): Optimize `Scope.setTag` bundle size and adjust test ([#18182](https://github.com/getsentry/sentry-javascript/pull/18182)) + +
## 10.25.0 diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index b6a7e01e7bfb..6350a0826572 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "10.25.0", + "version": "10.26.0", "main": "index.js", "license": "MIT", "engines": { @@ -43,7 +43,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.53.2", "@sentry-internal/rrweb": "2.34.0", - "@sentry/browser": "10.25.0", + "@sentry/browser": "10.26.0", "@supabase/supabase-js": "2.49.3", "axios": "^1.12.2", "babel-loader": "^8.2.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 5de03ee2a113..72928caf3708 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "10.25.0", + "version": "10.26.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/bundler-tests/package.json b/dev-packages/bundler-tests/package.json index a7850b3c1cc6..14bfa512f433 100644 --- a/dev-packages/bundler-tests/package.json +++ b/dev-packages/bundler-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundler-tests", - "version": "10.25.0", + "version": "10.26.0", "description": "Bundler tests for Sentry Browser SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bundler-tests", @@ -13,7 +13,7 @@ }, "dependencies": { "@rollup/plugin-node-resolve": "^15.2.3", - "@sentry/browser": "10.25.0", + "@sentry/browser": "10.26.0", "rollup": "^4.0.0", "vite": "^5.0.0", "vitest": "^3.2.4", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index c64bdf36eb9a..22f0c16d2680 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/cloudflare-integration-tests/package.json b/dev-packages/cloudflare-integration-tests/package.json index c791a224a2cc..e4af74f7edfe 100644 --- a/dev-packages/cloudflare-integration-tests/package.json +++ b/dev-packages/cloudflare-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/cloudflare-integration-tests", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" @@ -13,12 +13,12 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@sentry/cloudflare": "10.25.0", - "@langchain/langgraph": "^1.0.1" + "@langchain/langgraph": "^1.0.1", + "@sentry/cloudflare": "10.26.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250922.0", - "@sentry-internal/test-utils": "10.25.0", + "@sentry-internal/test-utils": "10.26.0", "eslint-plugin-regexp": "^1.15.0", "vitest": "^3.2.4", "wrangler": "4.22.0" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 556891ca2d55..7712bbac10ee 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 4b536a69fc1d..9e99c2b66ae0 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json index bf4bf9f3578a..fe755f16cc6d 100644 --- a/dev-packages/node-core-integration-tests/package.json +++ b/dev-packages/node-core-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-core-integration-tests", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" @@ -34,8 +34,8 @@ "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.25.0", - "@sentry/node-core": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node-core": "10.26.0", "body-parser": "^1.20.3", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 0bc5b8b83b1c..1799e7c9b306 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" @@ -36,9 +36,9 @@ "@nestjs/core": "^11", "@nestjs/platform-express": "^11", "@prisma/client": "6.15.0", - "@sentry/aws-serverless": "10.25.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", + "@sentry/aws-serverless": "10.26.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", @@ -83,7 +83,7 @@ "yargs": "^16.2.0" }, "devDependencies": { - "@sentry-internal/test-utils": "10.25.0", + "@sentry-internal/test-utils": "10.26.0", "@types/amqplib": "^0.10.5", "@types/node-cron": "^3.0.11", "@types/node-schedule": "^2.1.7", diff --git a/dev-packages/node-overhead-gh-action/package.json b/dev-packages/node-overhead-gh-action/package.json index 02dca29a291f..498cefecf053 100644 --- a/dev-packages/node-overhead-gh-action/package.json +++ b/dev-packages/node-overhead-gh-action/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-overhead-gh-action", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" @@ -23,7 +23,7 @@ "fix": "eslint . --format stylish --fix" }, "dependencies": { - "@sentry/node": "10.25.0", + "@sentry/node": "10.26.0", "express": "^4.21.1", "mysql2": "^3.14.4" }, diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index 60237502ec71..62cec9c7f9f9 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "10.25.0", + "version": "10.26.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index e29d747dcbaa..5de80ac560c7 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "10.25.0", + "version": "10.26.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index c8c0c09b353d..7d0400748c94 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "10.25.0", + "version": "10.26.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -48,7 +48,7 @@ }, "devDependencies": { "@playwright/test": "~1.53.2", - "@sentry/core": "10.25.0", + "@sentry/core": "10.26.0", "eslint-plugin-regexp": "^1.15.0" }, "volta": { diff --git a/lerna.json b/lerna.json index 3b70b74ac476..7363d9e257e2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "10.25.0", + "version": "10.26.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 6102320de760..01912cb13f79 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index c8ff489ca77b..32a2cf0e3833 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", "@sentry/vite-plugin": "^4.1.0" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 7ee6a0a50d97..cd0ad16d9e7c 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/aws-serverless", @@ -69,9 +69,9 @@ "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-aws-sdk": "0.59.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/node-core": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/node-core": "10.26.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/aws-serverless/src/init.ts b/packages/aws-serverless/src/init.ts index 25180a41f6e6..29ec1a16262a 100644 --- a/packages/aws-serverless/src/init.ts +++ b/packages/aws-serverless/src/init.ts @@ -1,8 +1,7 @@ import type { Integration, Options } from '@sentry/core'; -import { applySdkMetadata, debug, getSDKSource } from '@sentry/core'; +import { applySdkMetadata, debug, envToBool, getSDKSource } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations } from '@sentry/node'; -import { envToBool } from '@sentry/node-core'; import { DEBUG_BUILD } from './debug-build'; import { awsIntegration } from './integration/aws'; import { awsLambdaIntegration } from './integration/awslambda'; diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 338175ef5ad5..117fec1f473c 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "10.25.0", + "version": "10.26.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index 555062e2744f..c7b21beb6766 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -44,14 +44,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "10.25.0", - "@sentry-internal/feedback": "10.25.0", - "@sentry-internal/replay": "10.25.0", - "@sentry-internal/replay-canvas": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry-internal/browser-utils": "10.26.0", + "@sentry-internal/feedback": "10.26.0", + "@sentry-internal/replay": "10.26.0", + "@sentry-internal/replay-canvas": "10.26.0", + "@sentry/core": "10.26.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "10.25.0", + "@sentry-internal/integration-shims": "10.26.0", "fake-indexeddb": "^6.2.4" }, "scripts": { diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index ea55174f340c..554d3dd2d6b5 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -70,9 +70,29 @@ type BrowserSpecificOptions = BrowserClientReplayOptions & * * Either set it to true, or provide a specific Spotlight Sidecar URL. * + * Alternatively, you can configure Spotlight using environment variables (checked in this order): + * - PUBLIC_SENTRY_SPOTLIGHT (SvelteKit, Astro, Qwik) + * - NEXT_PUBLIC_SENTRY_SPOTLIGHT (Next.js) + * - VITE_SENTRY_SPOTLIGHT (Vite) + * - NUXT_PUBLIC_SENTRY_SPOTLIGHT (Nuxt) + * - REACT_APP_SENTRY_SPOTLIGHT (Create React App) + * - VUE_APP_SENTRY_SPOTLIGHT (Vue CLI) + * - GATSBY_SENTRY_SPOTLIGHT (Gatsby) + * - SENTRY_SPOTLIGHT (fallback for non-framework setups) + * + * Framework-specific vars have higher priority to support Docker Compose setups where + * backend uses SENTRY_SPOTLIGHT with Docker hostnames while frontend needs localhost. + * + * Precedence rules: + * - If this option is `false`, Spotlight is disabled (env vars ignored) + * - If this option is a string URL, that URL is used (env vars ignored) + * - If this option is `true` and env var is a URL, the env var URL is used + * - If this option is `undefined`, the env var value is used (if set) + * * More details: https://spotlightjs.com/ * * IMPORTANT: Only set this option to `true` while developing, not in production! + * Spotlight is automatically excluded from production bundles. */ spotlight?: boolean | string; }; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 800c1b701352..11a8ccf19548 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -5,6 +5,7 @@ import { getIntegrationsToSetup, inboundFiltersIntegration, initAndBind, + resolveSpotlightOptions, stackParserFromStackParserOptions, } from '@sentry/core'; import type { BrowserClientOptions, BrowserOptions } from './client'; @@ -19,6 +20,7 @@ import { spotlightBrowserIntegration } from './integrations/spotlight'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; import { checkAndWarnIfIsEmbeddedBrowserExtension } from './utils/detectBrowserExtension'; +import { getSpotlightConfig } from './utils/spotlightConfig'; /** Get the default integrations for the browser SDK. */ export function getDefaultIntegrations(_options: Options): Integration[] { @@ -95,11 +97,16 @@ export function init(options: BrowserOptions = {}): Client | undefined { options.defaultIntegrations == null ? getDefaultIntegrations(options) : options.defaultIntegrations; /* rollup-include-development-only */ - if (options.spotlight) { + // Resolve Spotlight configuration with proper precedence + const envSpotlight = getSpotlightConfig(); + // resolveSpotlightOptions is the single source of truth that ensures empty strings are never used + const spotlightValue = resolveSpotlightOptions(options.spotlight, envSpotlight); + + if (spotlightValue) { if (!defaultIntegrations) { defaultIntegrations = []; } - const args = typeof options.spotlight === 'string' ? { sidecarUrl: options.spotlight } : undefined; + const args = typeof spotlightValue === 'string' ? { sidecarUrl: spotlightValue } : undefined; defaultIntegrations.push(spotlightBrowserIntegration(args)); } /* rollup-include-development-only-end */ diff --git a/packages/browser/src/utils/env.ts b/packages/browser/src/utils/env.ts new file mode 100644 index 000000000000..94fb211fc644 --- /dev/null +++ b/packages/browser/src/utils/env.ts @@ -0,0 +1,39 @@ +/** + * Safely gets an environment variable value with defensive guards for browser environments. + * Checks both process.env and import.meta.env to support different bundlers: + * - process.env: Webpack, Next.js, Create React App, Parcel + * - import.meta.env: Vite, Astro, SvelteKit + * + * @param key - The environment variable key to look up + * @returns The value of the environment variable or undefined if not found + */ +export function getEnvValue(key: string): string | undefined { + // Check process.env first (Webpack, Next.js, CRA, etc.) + try { + if (typeof process !== 'undefined' && process.env) { + const value = process.env[key]; + if (value !== undefined) { + return value; + } + } + } catch (e) { + // Silently ignore - process might not be accessible or might throw in some environments + } + + // Check import.meta.env (Vite, Astro, SvelteKit, etc.) + try { + // @ts-expect-error import.meta.env might not exist in all environments + if (typeof import.meta !== 'undefined' && import.meta.env) { + // @ts-expect-error import.meta.env is typed differently in different environments + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const value = import.meta.env[key]; + if (value !== undefined) { + return value; + } + } + } catch (e) { + // Silently ignore - import.meta.env might not be accessible or might throw + } + + return undefined; +} diff --git a/packages/browser/src/utils/spotlightConfig.ts b/packages/browser/src/utils/spotlightConfig.ts new file mode 100644 index 000000000000..0fd855c6cc6b --- /dev/null +++ b/packages/browser/src/utils/spotlightConfig.ts @@ -0,0 +1,66 @@ +import { debug, envToBool } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; +import { getEnvValue } from './env'; + +/** + * Environment variable keys to check for Spotlight configuration, in priority order. + * The first one found with a value will be used. + * + * IMPORTANT: Framework-specific variables (PUBLIC_*, NEXT_PUBLIC_*, etc.) are prioritized + * over the generic SENTRY_SPOTLIGHT to support Docker Compose setups where: + * - Backend services need SENTRY_SPOTLIGHT=http://host.internal.docker:8969/stream + * - Frontend code needs localhost (via framework-specific vars like NEXT_PUBLIC_SENTRY_SPOTLIGHT=http://localhost:8969/stream) + * + * SENTRY_SPOTLIGHT is kept as a fallback for: + * - Simple non-Docker setups + * - Remote Spotlight instances when no framework-specific var is set + */ +const SPOTLIGHT_ENV_KEYS = [ + 'PUBLIC_SENTRY_SPOTLIGHT', // SvelteKit, Astro, Qwik + 'NEXT_PUBLIC_SENTRY_SPOTLIGHT', // Next.js + 'VITE_SENTRY_SPOTLIGHT', // Vite + 'NUXT_PUBLIC_SENTRY_SPOTLIGHT', // Nuxt + 'REACT_APP_SENTRY_SPOTLIGHT', // Create React App + 'VUE_APP_SENTRY_SPOTLIGHT', // Vue CLI + 'GATSBY_SENTRY_SPOTLIGHT', // Gatsby + 'SENTRY_SPOTLIGHT', // Fallback/base name - works in Parcel, Webpack, Rspack, Rollup, Rolldown, Node.js +] as const; + +/** + * Gets the Spotlight configuration from environment variables. + * Checks multiple environment variable prefixes in priority order to support + * different bundlers and frameworks. + * + * @returns The resolved Spotlight configuration (boolean | string | undefined) + */ +export function getSpotlightConfig(): boolean | string | undefined { + for (const key of SPOTLIGHT_ENV_KEYS) { + const value = getEnvValue(key); + + if (value !== undefined) { + // Try to parse as boolean first (strict mode) + const boolValue = envToBool(value, { strict: true }); + + if (boolValue !== null) { + // It's a valid boolean value + if (DEBUG_BUILD) { + debug.log(`[Spotlight] Found ${key}=${String(boolValue)} in environment variables`); + } + return boolValue; + } + + // Not a boolean, treat as custom URL string + // Note: Empty/whitespace strings are intentionally returned as-is. The resolveSpotlightOptions + // function is the single source of truth for filtering these and ensuring empty strings are + // NEVER used (returns undefined instead). + if (DEBUG_BUILD) { + debug.log(`[Spotlight] Found ${key}=${value} (custom URL) in environment variables`); + } + return value; + } + } + + // No Spotlight configuration found in environment + // Note: Implicit return of undefined saves bytes and is tree-shaken in production builds + return undefined; +} diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index b7972797182f..893624560c24 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -234,6 +234,154 @@ describe('init', () => { }); }); + describe('Spotlight environment variable support', () => { + let originalProcess: typeof globalThis.process | undefined; + + afterEach(() => { + if (originalProcess !== undefined) { + globalThis.process = originalProcess; + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (globalThis as any).process; + } + }); + + it('uses environment variable when options.spotlight is undefined', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'true', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should be added + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeDefined(); + }); + + it('does not add Spotlight when environment variable is false', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should NOT be added + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeUndefined(); + }); + + it('options.spotlight=false takes precedence over environment variable', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'true', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: false }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should NOT be added even though env var is true + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeUndefined(); + }); + + it('options.spotlight=url takes precedence over environment variable', () => { + originalProcess = globalThis.process; + const customUrl = 'http://custom:1234/stream'; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'http://env:5678/stream', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: customUrl }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should be added (we can't easily check the URL here without deeper inspection) + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeDefined(); + }); + + it('uses environment variable URL when options.spotlight=true', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'http://env:5678/stream', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: true }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should be added + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeDefined(); + }); + + it('respects priority order: PUBLIC_SENTRY_SPOTLIGHT over SENTRY_SPOTLIGHT', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + PUBLIC_SENTRY_SPOTLIGHT: 'true', + SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should be added (PUBLIC_SENTRY_SPOTLIGHT=true wins) + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeDefined(); + }); + + it('uses framework-specific prefix when base is not set', () => { + originalProcess = globalThis.process; + globalThis.process = { + env: { + NEXT_PUBLIC_SENTRY_SPOTLIGHT: 'true', + } as Record, + } as NodeJS.Process; + + // @ts-expect-error this is fine for testing + const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, spotlight: undefined }); + init(options); + + const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; + // Spotlight integration should be added + const spotlightIntegration = optionsPassed?.integrations.find((i: Integration) => i.name === 'SpotlightBrowser'); + expect(spotlightIntegration).toBeDefined(); + }); + }); + it('returns a client from init', () => { const client = init(); expect(client).not.toBeUndefined(); diff --git a/packages/browser/test/utils/env.test.ts b/packages/browser/test/utils/env.test.ts new file mode 100644 index 000000000000..6c88985da904 --- /dev/null +++ b/packages/browser/test/utils/env.test.ts @@ -0,0 +1,90 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getEnvValue } from '../../src/utils/env'; + +describe('getEnvValue', () => { + let originalProcess: typeof globalThis.process | undefined; + + beforeEach(() => { + // Store original values + originalProcess = globalThis.process; + }); + + afterEach(() => { + // Restore original values + if (originalProcess !== undefined) { + globalThis.process = originalProcess; + } else { + delete (globalThis as any).process; + } + }); + + it('returns value from process.env when available', () => { + globalThis.process = { + env: { + TEST_VAR: 'test-value', + }, + } as any; + + expect(getEnvValue('TEST_VAR')).toBe('test-value'); + }); + + it('returns undefined when process.env does not exist', () => { + delete (globalThis as any).process; + + expect(getEnvValue('NONEXISTENT')).toBeUndefined(); + }); + + it('returns undefined when variable does not exist in process.env', () => { + globalThis.process = { + env: {}, + } as any; + + expect(getEnvValue('NONEXISTENT')).toBeUndefined(); + }); + + it('handles missing process object gracefully', () => { + delete (globalThis as any).process; + + expect(() => getEnvValue('TEST_VAR')).not.toThrow(); + expect(getEnvValue('TEST_VAR')).toBeUndefined(); + }); + + it('handles missing process.env gracefully', () => { + globalThis.process = {} as any; + + expect(() => getEnvValue('TEST_VAR')).not.toThrow(); + expect(getEnvValue('TEST_VAR')).toBeUndefined(); + }); + + it('handles process.env access throwing an error', () => { + globalThis.process = { + get env() { + throw new Error('Access denied'); + }, + } as any; + + expect(() => getEnvValue('TEST_VAR')).not.toThrow(); + expect(getEnvValue('TEST_VAR')).toBeUndefined(); + }); + + it('returns empty string when value is empty string in process.env', () => { + globalThis.process = { + env: { + EMPTY_VAR: '', + }, + } as any; + + expect(getEnvValue('EMPTY_VAR')).toBe(''); + }); + + // Note: import.meta.env support cannot be easily unit tested because import.meta + // is a read-only compile-time construct that cannot be mocked. The import.meta.env + // functionality is tested via e2e tests with real Vite-based frameworks (Vue, Astro, etc.) + // + // The implementation safely checks for import.meta.env existence and will use it + // when available in Vite/Astro/SvelteKit builds. +}); diff --git a/packages/browser/test/utils/spotlightConfig.test.ts b/packages/browser/test/utils/spotlightConfig.test.ts new file mode 100644 index 000000000000..a540c6be8ce6 --- /dev/null +++ b/packages/browser/test/utils/spotlightConfig.test.ts @@ -0,0 +1,268 @@ +/** + * @vitest-environment jsdom + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { getSpotlightConfig } from '../../src/utils/spotlightConfig'; + +describe('getSpotlightConfig', () => { + let originalProcess: typeof globalThis.process | undefined; + + beforeEach(() => { + originalProcess = globalThis.process; + }); + + afterEach(() => { + if (originalProcess !== undefined) { + globalThis.process = originalProcess; + } else { + delete (globalThis as typeof globalThis & { process?: NodeJS.Process }).process; + } + }); + + it('returns undefined when no environment variables are set', () => { + globalThis.process = { + env: {}, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBeUndefined(); + }); + + it('returns boolean true when SENTRY_SPOTLIGHT=true', () => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'true', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('returns boolean false when SENTRY_SPOTLIGHT=false', () => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(false); + }); + + it('returns URL string when SENTRY_SPOTLIGHT is a URL', () => { + const customUrl = 'http://localhost:9999/stream'; + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: customUrl, + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(customUrl); + }); + + it('returns empty string when SENTRY_SPOTLIGHT is an empty string (filtered by resolveSpotlightOptions)', () => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: '', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(''); + }); + + it('returns whitespace string when SENTRY_SPOTLIGHT is whitespace only (filtered by resolveSpotlightOptions)', () => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: ' ', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(' '); + }); + + it('parses various truthy values correctly', () => { + const truthyValues = ['true', '1', 'yes', 'on', 't', 'y', 'TRUE', 'YES']; + + truthyValues.forEach(value => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: value, + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + }); + + it('parses various falsy values correctly', () => { + const falsyValues = ['false', '0', 'no', 'off', 'f', 'n', 'FALSE', 'NO']; + + falsyValues.forEach(value => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: value, + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(false); + }); + }); + + describe('priority order', () => { + it('prioritizes PUBLIC_SENTRY_SPOTLIGHT over SENTRY_SPOTLIGHT', () => { + globalThis.process = { + env: { + PUBLIC_SENTRY_SPOTLIGHT: 'true', + SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('prioritizes PUBLIC_SENTRY_SPOTLIGHT when SENTRY_SPOTLIGHT is not set', () => { + globalThis.process = { + env: { + PUBLIC_SENTRY_SPOTLIGHT: 'true', + NEXT_PUBLIC_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('prioritizes NEXT_PUBLIC_SENTRY_SPOTLIGHT when higher priority vars not set', () => { + globalThis.process = { + env: { + NEXT_PUBLIC_SENTRY_SPOTLIGHT: 'true', + VITE_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('prioritizes VITE_SENTRY_SPOTLIGHT when higher priority vars not set', () => { + globalThis.process = { + env: { + VITE_SENTRY_SPOTLIGHT: 'true', + NUXT_PUBLIC_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('prioritizes NUXT_PUBLIC_SENTRY_SPOTLIGHT when higher priority vars not set', () => { + globalThis.process = { + env: { + NUXT_PUBLIC_SENTRY_SPOTLIGHT: 'true', + REACT_APP_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('uses REACT_APP_SENTRY_SPOTLIGHT when higher priority vars not set', () => { + globalThis.process = { + env: { + REACT_APP_SENTRY_SPOTLIGHT: 'true', + VUE_APP_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('uses VUE_APP_SENTRY_SPOTLIGHT when higher priority vars not set', () => { + globalThis.process = { + env: { + VUE_APP_SENTRY_SPOTLIGHT: 'true', + GATSBY_SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('uses GATSBY_SENTRY_SPOTLIGHT when all higher priority vars not set', () => { + globalThis.process = { + env: { + GATSBY_SENTRY_SPOTLIGHT: 'true', + SENTRY_SPOTLIGHT: 'false', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('uses SENTRY_SPOTLIGHT as fallback when no framework-specific vars are set', () => { + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: 'true', + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(true); + }); + + it('returns first found value in priority order with URLs', () => { + const highPriorityUrl = 'http://high-priority:8969/stream'; + const lowPriorityUrl = 'http://low-priority:8969/stream'; + + globalThis.process = { + env: { + PUBLIC_SENTRY_SPOTLIGHT: highPriorityUrl, + GATSBY_SENTRY_SPOTLIGHT: lowPriorityUrl, + } as Record, + } as NodeJS.Process; + + expect(getSpotlightConfig()).toBe(highPriorityUrl); + }); + + it('prioritizes framework-specific URL over SENTRY_SPOTLIGHT URL (Docker Compose scenario)', () => { + // Simulates Docker Compose setup where: + // - SENTRY_SPOTLIGHT is set for backend services with Docker hostname + // - Framework-specific var is set for frontend with localhost + const dockerHostUrl = 'http://host.docker.internal:8969/stream'; + const localhostUrl = 'http://localhost:8969/stream'; + + globalThis.process = { + env: { + NEXT_PUBLIC_SENTRY_SPOTLIGHT: localhostUrl, + SENTRY_SPOTLIGHT: dockerHostUrl, + } as Record, + } as NodeJS.Process; + + // Framework-specific var should be used, not SENTRY_SPOTLIGHT + expect(getSpotlightConfig()).toBe(localhostUrl); + }); + + it('uses SENTRY_SPOTLIGHT URL when no framework-specific vars are set (remote Spotlight)', () => { + const remoteUrl = 'http://remote-spotlight.example.com:8969/stream'; + + globalThis.process = { + env: { + SENTRY_SPOTLIGHT: remoteUrl, + } as Record, + } as NodeJS.Process; + + // Should use SENTRY_SPOTLIGHT as fallback + expect(getSpotlightConfig()).toBe(remoteUrl); + }); + }); + + it('handles missing process object gracefully', () => { + delete (globalThis as typeof globalThis & { process?: NodeJS.Process }).process; + + expect(() => getSpotlightConfig()).not.toThrow(); + expect(getSpotlightConfig()).toBeUndefined(); + }); + + it('handles missing process.env gracefully', () => { + globalThis.process = {} as NodeJS.Process; + + expect(() => getSpotlightConfig()).not.toThrow(); + expect(getSpotlightConfig()).toBeUndefined(); + }); +}); diff --git a/packages/bun/package.json b/packages/bun/package.json index 288e327bf6eb..fcd538862c17 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0" + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index a620dc84b3d9..ee8cba264e5f 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -50,7 +50,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index e45152996f0f..8ffc256e2f26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "10.25.0", + "version": "10.26.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 014a411d0265..d3f36551f94e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -272,6 +272,9 @@ export { } from './utils/tracing'; export { getSDKSource, isBrowserBundle } from './utils/env'; export type { SdkSource } from './utils/env'; +export { envToBool } from './utils/envToBool'; +export type { BoolCastOptions, StrictBoolCast, LooseBoolCast } from './utils/envToBool'; +export { resolveSpotlightOptions } from './utils/resolveSpotlightOptions'; export { addItemToEnvelope, createAttachmentEnvelopeItem, diff --git a/packages/node-core/src/utils/envToBool.ts b/packages/core/src/utils/envToBool.ts similarity index 100% rename from packages/node-core/src/utils/envToBool.ts rename to packages/core/src/utils/envToBool.ts diff --git a/packages/core/src/utils/resolveSpotlightOptions.ts b/packages/core/src/utils/resolveSpotlightOptions.ts new file mode 100644 index 000000000000..520b1f4d6647 --- /dev/null +++ b/packages/core/src/utils/resolveSpotlightOptions.ts @@ -0,0 +1,36 @@ +/** + * Resolves the final spotlight configuration based on options and environment variables. + * Implements the precedence rules from the Spotlight spec. + * + * This is the single source of truth for filtering empty/whitespace strings - it ensures that + * empty strings are NEVER returned (returns undefined instead). All callers can rely on this + * guarantee when handling spotlight configuration. + * + * @param optionsSpotlight - The spotlight option from user config (false | true | string | undefined) + * @param envSpotlight - The spotlight value from environment variables (false | true | string | undefined) + * @returns The resolved spotlight configuration (false | true | string | undefined) - NEVER an empty string + */ +export function resolveSpotlightOptions( + optionsSpotlight: boolean | string | undefined, + envSpotlight: boolean | string | undefined, +): boolean | string | undefined { + if (optionsSpotlight === false) { + // Explicitly disabled - ignore env vars + return false; + } + + if (typeof optionsSpotlight === 'string') { + // Custom URL provided - ignore env vars + // Treat empty strings as undefined to prevent invalid URL connections + return optionsSpotlight.trim() === '' ? undefined : optionsSpotlight; + } + + // optionsSpotlight is true or undefined + const envBool = typeof envSpotlight === 'boolean' ? envSpotlight : undefined; + // Treat empty/whitespace-only strings as undefined to prevent invalid URL connections + const envUrl = typeof envSpotlight === 'string' && envSpotlight.trim() !== '' ? envSpotlight : undefined; + + return optionsSpotlight === true + ? (envUrl ?? true) // true: use env URL if present, otherwise true + : (envBool ?? envUrl); // undefined: use env var (bool or URL) +} diff --git a/packages/node-core/test/utils/envToBool.test.ts b/packages/core/test/utils/envToBool.test.ts similarity index 100% rename from packages/node-core/test/utils/envToBool.test.ts rename to packages/core/test/utils/envToBool.test.ts diff --git a/packages/core/test/utils/resolveSpotlightOptions.test.ts b/packages/core/test/utils/resolveSpotlightOptions.test.ts new file mode 100644 index 000000000000..27b1a61dddd5 --- /dev/null +++ b/packages/core/test/utils/resolveSpotlightOptions.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; +import { resolveSpotlightOptions } from '../../src/utils/resolveSpotlightOptions'; + +describe('resolveSpotlightOptions', () => { + it('returns false when options.spotlight === false, regardless of env', () => { + expect(resolveSpotlightOptions(false, undefined)).toBe(false); + expect(resolveSpotlightOptions(false, true)).toBe(false); + expect(resolveSpotlightOptions(false, false)).toBe(false); + expect(resolveSpotlightOptions(false, 'http://localhost:8969')).toBe(false); + }); + + it('returns custom URL when options.spotlight is a string, regardless of env', () => { + const customUrl = 'http://custom:1234/stream'; + expect(resolveSpotlightOptions(customUrl, undefined)).toBe(customUrl); + expect(resolveSpotlightOptions(customUrl, true)).toBe(customUrl); + expect(resolveSpotlightOptions(customUrl, false)).toBe(customUrl); + expect(resolveSpotlightOptions(customUrl, 'http://other:5678')).toBe(customUrl); + }); + + it('returns env URL when options.spotlight === true and env is a URL', () => { + const envUrl = 'http://localhost:8969/stream'; + expect(resolveSpotlightOptions(true, envUrl)).toBe(envUrl); + }); + + it('returns true when options.spotlight === true and env is true', () => { + expect(resolveSpotlightOptions(true, true)).toBe(true); + }); + + it('returns true when options.spotlight === true and env is false', () => { + expect(resolveSpotlightOptions(true, false)).toBe(true); + }); + + it('returns true when options.spotlight === true and env is undefined', () => { + expect(resolveSpotlightOptions(true, undefined)).toBe(true); + }); + + it('returns env boolean when options.spotlight === undefined and env is boolean', () => { + expect(resolveSpotlightOptions(undefined, true)).toBe(true); + expect(resolveSpotlightOptions(undefined, false)).toBe(false); + }); + + it('returns env URL when options.spotlight === undefined and env is a URL', () => { + const envUrl = 'http://localhost:8969/stream'; + expect(resolveSpotlightOptions(undefined, envUrl)).toBe(envUrl); + }); + + it('returns undefined when both options.spotlight and env are undefined', () => { + expect(resolveSpotlightOptions(undefined, undefined)).toBe(undefined); + }); + + it('prioritizes env URL over env boolean when options.spotlight === undefined', () => { + // This shouldn't happen in practice, but tests the logic path + // In reality, envSpotlight will be either boolean, string, or undefined + const envUrl = 'http://localhost:8969/stream'; + expect(resolveSpotlightOptions(undefined, envUrl)).toBe(envUrl); + }); + + describe('empty string handling - NEVER returns empty strings', () => { + it('returns undefined (never empty string) when options.spotlight is an empty string', () => { + expect(resolveSpotlightOptions('', undefined)).toBeUndefined(); + expect(resolveSpotlightOptions('', true)).toBeUndefined(); + expect(resolveSpotlightOptions('', 'http://env:8969')).toBeUndefined(); + }); + + it('returns undefined (never empty string) when options.spotlight is whitespace only', () => { + expect(resolveSpotlightOptions(' ', undefined)).toBeUndefined(); + expect(resolveSpotlightOptions('\t\n', true)).toBeUndefined(); + }); + + it('returns undefined (never empty string) when env is an empty string and options.spotlight is undefined', () => { + expect(resolveSpotlightOptions(undefined, '')).toBeUndefined(); + }); + + it('returns undefined (never empty string) when env is whitespace only and options.spotlight is undefined', () => { + expect(resolveSpotlightOptions(undefined, ' ')).toBeUndefined(); + expect(resolveSpotlightOptions(undefined, '\t\n')).toBeUndefined(); + }); + + it('returns true when options.spotlight is true and env is empty string (filters out empty env)', () => { + expect(resolveSpotlightOptions(true, '')).toBe(true); + expect(resolveSpotlightOptions(true, ' ')).toBe(true); + }); + + it('returns valid URL when options.spotlight is valid URL even if env is empty (filters out empty env)', () => { + const validUrl = 'http://localhost:8969/stream'; + expect(resolveSpotlightOptions(validUrl, '')).toBe(validUrl); + expect(resolveSpotlightOptions(validUrl, ' ')).toBe(validUrl); + }); + + it('NEVER returns empty string - comprehensive check of all combinations', () => { + // Test all possible combinations to ensure empty strings are never returned + const emptyValues = ['', ' ', '\t\n', ' \t \n ']; + const nonEmptyValues = [false, true, undefined, 'http://localhost:8969']; + + // Empty options.spotlight with any env + for (const emptyOption of emptyValues) { + for (const env of [...emptyValues, ...nonEmptyValues]) { + const result = resolveSpotlightOptions(emptyOption, env); + expect(result).not.toBe(''); + // Only test regex on strings + if (typeof result === 'string') { + expect(result).not.toMatch(/^\s+$/); + } + } + } + + // Any options.spotlight with empty env + for (const option of [...emptyValues, ...nonEmptyValues]) { + for (const emptyEnv of emptyValues) { + const result = resolveSpotlightOptions(option, emptyEnv); + expect(result).not.toBe(''); + // Only test regex on strings + if (typeof result === 'string') { + expect(result).not.toMatch(/^\s+$/); + } + } + } + }); + }); +}); diff --git a/packages/deno/package.json b/packages/deno/package.json index 814d76142cc3..04ac8f592fd1 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -25,7 +25,7 @@ ], "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "scripts": { "deno-types": "node ./scripts/download-deno-types.mjs", diff --git a/packages/ember/package.json b/packages/ember/package.json index 333eda5345d5..69a111ab54a9 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.27.7", "@embroider/macros": "^1.16.0", - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 63c6f926f934..d45a301e1590 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "10.25.0", - "@sentry-internal/typescript": "10.25.0", + "@sentry-internal/eslint-plugin-sdk": "10.26.0", + "@sentry-internal/typescript": "10.26.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index f8d9f4c615ee..bad2ae248475 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index aa65e00d8ab5..7c9899ffd305 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "10.25.0", + "version": "10.26.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 1da49b3a4dca..8236ee72e0e9 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0", - "@sentry/react": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/react": "10.26.0", "@sentry/webpack-plugin": "^4.1.1" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index c742b5a3a154..6afb10e14c42 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index a2535fc2b9b4..8569a5b837f6 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "10.25.0", + "version": "10.26.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -56,7 +56,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 80fdb673cc6d..53c3064ed08f 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -49,8 +49,8 @@ "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-nestjs-core": "0.50.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0" + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 47adab59a32d..15b6a2bf3040 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,13 +79,13 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "10.25.0", + "@sentry-internal/browser-utils": "10.26.0", "@sentry/bundler-plugin-core": "^4.3.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/opentelemetry": "10.25.0", - "@sentry/react": "10.25.0", - "@sentry/vercel-edge": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/opentelemetry": "10.26.0", + "@sentry/react": "10.26.0", + "@sentry/vercel-edge": "10.26.0", "@sentry/webpack-plugin": "^4.3.0", "resolve": "1.22.8", "rollup": "^4.35.0", diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 43af0a610e61..89dbe5461165 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-core", - "version": "10.25.0", + "version": "10.26.0", "description": "Sentry Node-Core SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core", @@ -67,8 +67,8 @@ }, "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", - "@sentry/core": "10.25.0", - "@sentry/opentelemetry": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/opentelemetry": "10.26.0", "import-in-the-middle": "^1.14.2" }, "devDependencies": { diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 7557d73c74a2..9b5286db37dd 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -43,7 +43,6 @@ export { initializeEsmLoader } from './sdk/esmLoader'; export { isCjs } from './utils/detection'; export { ensureIsWrapped } from './utils/ensureIsWrapped'; export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext'; -export { envToBool } from './utils/envToBool'; export { makeNodeTransport, type NodeTransportOptions } from './transports'; export type { HTTPModuleRequestIncomingMessage } from './transports/http-module'; export { NodeClient } from './sdk/client'; diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index 0814ab401535..f8010e09b5c6 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -4,6 +4,7 @@ import { consoleIntegration, consoleSandbox, debug, + envToBool, functionToStringIntegration, getCurrentScope, getIntegrationsToSetup, @@ -13,6 +14,7 @@ import { linkedErrorsIntegration, propagationContextFromHeaders, requestDataIntegration, + resolveSpotlightOptions, stackParserFromStackParserOptions, } from '@sentry/core'; import { @@ -37,7 +39,6 @@ import { systemErrorIntegration } from '../integrations/systemError'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; import { isCjs } from '../utils/detection'; -import { envToBool } from '../utils/envToBool'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initializeEsmLoader } from './esmLoader'; @@ -184,21 +185,11 @@ function getClientOptions( const release = getRelease(options.release); // Parse spotlight configuration with proper precedence per spec - let spotlight: boolean | string | undefined; - if (options.spotlight === false) { - spotlight = false; - } else if (typeof options.spotlight === 'string') { - spotlight = options.spotlight; - } else { - // options.spotlight is true or undefined - const envBool = envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }); - const envUrl = envBool === null && process.env.SENTRY_SPOTLIGHT ? process.env.SENTRY_SPOTLIGHT : undefined; - - spotlight = - options.spotlight === true - ? (envUrl ?? true) // true: use env URL if present, otherwise true - : (envBool ?? envUrl); // undefined: use env var (bool or URL) - } + const envBool = envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }); + const envSpotlight = envBool !== null ? envBool : process.env.SENTRY_SPOTLIGHT; + // Note: resolveSpotlightOptions is the single source of truth for filtering empty/whitespace strings + // and ensures that empty strings are NEVER returned (returns undefined instead) + const spotlight = resolveSpotlightOptions(options.spotlight, envSpotlight); const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); diff --git a/packages/node-core/test/sdk/init.test.ts b/packages/node-core/test/sdk/init.test.ts index 1332b89abcd6..04cfc75649f1 100644 --- a/packages/node-core/test/sdk/init.test.ts +++ b/packages/node-core/test/sdk/init.test.ts @@ -323,6 +323,24 @@ describe('init()', () => { expect(client?.getOptions().spotlight).toBe(true); expect(client?.getOptions().integrations.some(integration => integration.name === 'Spotlight')).toBe(true); }); + + it('treats empty string env var as undefined (no spotlight)', () => { + process.env.SENTRY_SPOTLIGHT = ''; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions().spotlight).toBeUndefined(); + expect(client?.getOptions().integrations.some(integration => integration.name === 'Spotlight')).toBe(false); + }); + + it('treats whitespace-only string env var as undefined (no spotlight)', () => { + process.env.SENTRY_SPOTLIGHT = ' '; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions().spotlight).toBeUndefined(); + expect(client?.getOptions().integrations.some(integration => integration.name === 'Spotlight')).toBe(false); + }); }); }); }); diff --git a/packages/node-native/package.json b/packages/node-native/package.json index 23ce0068019c..9a4c40818556 100644 --- a/packages/node-native/package.json +++ b/packages/node-native/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-native", - "version": "10.25.0", + "version": "10.26.0", "description": "Native Tools for the Official Sentry Node.js SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-native", @@ -64,8 +64,8 @@ }, "dependencies": { "@sentry-internal/node-native-stacktrace": "^0.2.2", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0" + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/node/package.json b/packages/node/package.json index 830fb510f9f9..6f0bec49c92a 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "10.25.0", + "version": "10.26.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -95,9 +95,9 @@ "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.15.0", - "@sentry/core": "10.25.0", - "@sentry/node-core": "10.25.0", - "@sentry/opentelemetry": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node-core": "10.26.0", + "@sentry/opentelemetry": "10.26.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" }, diff --git a/packages/node/src/preload.ts b/packages/node/src/preload.ts index 615ab0c1a008..7684449ee524 100644 --- a/packages/node/src/preload.ts +++ b/packages/node/src/preload.ts @@ -1,4 +1,4 @@ -import { envToBool } from '@sentry/node-core'; +import { envToBool } from '@sentry/core'; import { preloadOpenTelemetry } from './sdk/initOtel'; const debug = envToBool(process.env.SENTRY_DEBUG); diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 3a9679553475..e054d1b9938a 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Nuxt", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -49,13 +49,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "10.25.0", - "@sentry/cloudflare": "10.25.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/cloudflare": "10.26.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", "@sentry/rollup-plugin": "^4.3.0", "@sentry/vite-plugin": "^4.3.0", - "@sentry/vue": "10.25.0" + "@sentry/vue": "10.26.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 94457ff83b65..95b7f54cd000 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 7040f0a53c21..e897b8d5611e 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -63,8 +63,8 @@ }, "dependencies": { "@sentry-internal/node-cpu-profiler": "^2.2.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0" + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 52dd1a71fee7..3d57ad54469f 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for React Router (Framework)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router", @@ -49,11 +49,11 @@ "@opentelemetry/core": "^2.1.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/browser": "10.25.0", + "@sentry/browser": "10.26.0", "@sentry/cli": "^2.56.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/react": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/react": "10.26.0", "@sentry/vite-plugin": "^4.1.0", "glob": "11.0.1" }, diff --git a/packages/react/package.json b/packages/react/package.json index c7e7163a9a5e..c51a7e2d3ee8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index 9a255af786ce..667f77d53648 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -69,9 +69,9 @@ "@opentelemetry/semantic-conventions": "^1.37.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.56.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/react": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/react": "10.26.0", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/replay-canvas/core b/packages/replay-canvas/core new file mode 100644 index 000000000000..c0595c9682e7 Binary files /dev/null and b/packages/replay-canvas/core differ diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index 491d919e55dc..63ee871ddfdf 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "10.25.0", + "version": "10.26.0", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -69,8 +69,8 @@ "@sentry-internal/rrweb": "2.40.0" }, "dependencies": { - "@sentry-internal/replay": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry-internal/replay": "10.26.0", + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index d2e1efbd84b7..af121d6a74c0 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "10.25.0", + "version": "10.26.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -81,7 +81,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.27.7", - "@sentry-internal/replay-worker": "10.25.0", + "@sentry-internal/replay-worker": "10.26.0", "@sentry-internal/rrweb": "2.40.0", "@sentry-internal/rrweb-snapshot": "2.40.0", "fflate": "0.8.2", @@ -90,8 +90,8 @@ "node-fetch": "^2.6.7" }, "dependencies": { - "@sentry-internal/browser-utils": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry-internal/browser-utils": "10.26.0", + "@sentry/core": "10.26.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index a0d25235c7d7..e9a968061841 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "10.25.0", + "version": "10.26.0", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index bdb3dbe50a15..c75a0eadb4ce 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -54,8 +54,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index bb4acc3404f4..8bb0e4e0471f 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -66,9 +66,9 @@ } }, "dependencies": { - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/solid": "10.25.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/solid": "10.26.0", "@sentry/vite-plugin": "^4.1.0" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dc8c662012c3..6e9cbdf5b54a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0", + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 01f88fcd60b9..7802db426793 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -48,10 +48,10 @@ }, "dependencies": { "@babel/parser": "7.26.9", - "@sentry/cloudflare": "10.25.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/svelte": "10.25.0", + "@sentry/cloudflare": "10.26.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/svelte": "10.26.0", "@sentry/vite-plugin": "^4.1.0", "magic-string": "0.30.7", "recast": "0.23.11", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 49c0bf12d971..03688bdd6aad 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart-react", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for TanStack Start React", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react", @@ -52,10 +52,10 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry-internal/browser-utils": "10.25.0", - "@sentry/core": "10.25.0", - "@sentry/node": "10.25.0", - "@sentry/react": "10.25.0" + "@sentry-internal/browser-utils": "10.26.0", + "@sentry/core": "10.26.0", + "@sentry/node": "10.26.0", + "@sentry/react": "10.26.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index 56e0c7992fea..3918ecd9a3fa 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart", - "version": "10.25.0", + "version": "10.26.0", "description": "Utilities for the Sentry TanStack Start SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart", diff --git a/packages/types/package.json b/packages/types/package.json index 1c53b6c05275..2287d86014da 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "10.25.0", + "version": "10.26.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -57,7 +57,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index c1c99704c346..ab02fe16f828 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "10.25.0", + "version": "10.26.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index c7c6db556ccb..d5c9ef78b8c1 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -41,14 +41,14 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/resources": "^2.1.0", - "@sentry/core": "10.25.0" + "@sentry/core": "10.26.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1", "@opentelemetry/core": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", - "@sentry/opentelemetry": "10.25.0" + "@sentry/opentelemetry": "10.26.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/vue/package.json b/packages/vue/package.json index 31bef1a870c8..abdb956018c6 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "10.25.0", + "version": "10.26.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index f7a97139c120..2e7883644276 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "10.25.0", + "version": "10.26.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "10.25.0", - "@sentry/core": "10.25.0" + "@sentry/browser": "10.26.0", + "@sentry/core": "10.26.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types",