diff --git a/CHANGELOG.md b/CHANGELOG.md index 411e7b5e547..282d7f6c82d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :bug: Bug Fixes +* refactor(resources): use runtime check for default service name [#6257](https://github.com/open-telemetry/opentelemetry-js/pull/6257) @overbalance + ### :books: Documentation ### :house: Internal diff --git a/bundler-tests/browser/nextjs-15-edge/.npmrc b/bundler-tests/browser/nextjs-15-edge/.npmrc new file mode 100644 index 00000000000..6c93bcba751 --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/.npmrc @@ -0,0 +1,2 @@ +package-lock=false +install-links=false diff --git a/bundler-tests/browser/nextjs-15-edge/app/layout.js b/bundler-tests/browser/nextjs-15-edge/app/layout.js new file mode 100644 index 00000000000..23ec8e7d9a6 --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/bundler-tests/browser/nextjs-15-edge/app/page.js b/bundler-tests/browser/nextjs-15-edge/app/page.js new file mode 100644 index 00000000000..d79505b586b --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/app/page.js @@ -0,0 +1,3 @@ +export default function Home() { + return

OTel Edge Runtime Test

; +} diff --git a/bundler-tests/browser/nextjs-15-edge/middleware.js b/bundler-tests/browser/nextjs-15-edge/middleware.js new file mode 100644 index 00000000000..00158646dfb --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/middleware.js @@ -0,0 +1,11 @@ +import { defaultServiceName } from '@opentelemetry/resources'; + +export function middleware(request) { + const serviceName = defaultServiceName(); + console.log('Service name:', serviceName); + return Response.next(); +} + +export const config = { + matcher: '/api/:path*', +}; diff --git a/bundler-tests/browser/nextjs-15-edge/next.config.js b/bundler-tests/browser/nextjs-15-edge/next.config.js new file mode 100644 index 00000000000..fbcff3c0a8a --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/next.config.js @@ -0,0 +1,19 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + webpack: (config, { dev }) => { + // Treat warnings as errors + config.plugins.push({ + apply: compiler => { + compiler.hooks.done.tap('FailOnWarnings', stats => { + if (stats.compilation.warnings.length > 0) { + console.error(stats.compilation.warnings.join('\n')); + throw new Error('Webpack build has warnings'); + } + }); + }, + }); + return config; + }, +}; + +export default nextConfig; diff --git a/bundler-tests/browser/nextjs-15-edge/package.json b/bundler-tests/browser/nextjs-15-edge/package.json new file mode 100644 index 00000000000..c23ac0836f7 --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/package.json @@ -0,0 +1,15 @@ +{ + "name": "nextjs-15-edge-bundle-test", + "type": "module", + "private": true, + "scripts": { + "build": "rm -rf .next && next build", + "test:bundle": "npm i && npm run build" + }, + "dependencies": { + "@opentelemetry/resources": "file:../../../packages/opentelemetry-resources", + "next": "^15", + "react": "^18", + "react-dom": "^18" + } +} diff --git a/bundler-tests/browser/nextjs-15-edge/project.json b/bundler-tests/browser/nextjs-15-edge/project.json new file mode 100644 index 00000000000..49d53202949 --- /dev/null +++ b/bundler-tests/browser/nextjs-15-edge/project.json @@ -0,0 +1,3 @@ +{ + "name": "nextjs-15-edge-bundle-test" +} diff --git a/bundler-tests/browser/nextjs-16-edge/.npmrc b/bundler-tests/browser/nextjs-16-edge/.npmrc new file mode 100644 index 00000000000..6c93bcba751 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/.npmrc @@ -0,0 +1,2 @@ +package-lock=false +install-links=false diff --git a/bundler-tests/browser/nextjs-16-edge/app/api/test/route.js b/bundler-tests/browser/nextjs-16-edge/app/api/test/route.js new file mode 100644 index 00000000000..0835276d060 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/app/api/test/route.js @@ -0,0 +1,10 @@ +import { defaultServiceName } from '@opentelemetry/resources'; + +export const runtime = 'edge'; + +export function GET(request) { + const serviceName = defaultServiceName(); + return new Response(JSON.stringify({ serviceName }), { + headers: { 'Content-Type': 'application/json' }, + }); +} diff --git a/bundler-tests/browser/nextjs-16-edge/app/layout.js b/bundler-tests/browser/nextjs-16-edge/app/layout.js new file mode 100644 index 00000000000..23ec8e7d9a6 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/bundler-tests/browser/nextjs-16-edge/app/page.js b/bundler-tests/browser/nextjs-16-edge/app/page.js new file mode 100644 index 00000000000..d79505b586b --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/app/page.js @@ -0,0 +1,3 @@ +export default function Home() { + return

OTel Edge Runtime Test

; +} diff --git a/bundler-tests/browser/nextjs-16-edge/next.config.js b/bundler-tests/browser/nextjs-16-edge/next.config.js new file mode 100644 index 00000000000..4678774e6d6 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/bundler-tests/browser/nextjs-16-edge/next.test.ts b/bundler-tests/browser/nextjs-16-edge/next.test.ts new file mode 100644 index 00000000000..4757b52eb57 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/next.test.ts @@ -0,0 +1,22 @@ +/** + * Next.js 16+ uses Turbopack, which lacks callbacks for build errors or warnings. + * This script runs the build and fails if Turbopack reports any warnings. + */ +import { execSync } from 'node:child_process'; +import path from 'node:path'; + +const testDir = path.dirname(new URL(import.meta.url).pathname); + +// Install dependencies and run build, capturing both stdout and stderr +const output = execSync('npm run build 2>&1', { + cwd: testDir, + encoding: 'utf-8', +}); + +const hasWarning = /Turbopack build encountered/.test(output); +if (hasWarning) { + console.error('Build produced warnings:\n', output); + process.exit(1); +} + +console.log('Build completed without warnings'); diff --git a/bundler-tests/browser/nextjs-16-edge/package.json b/bundler-tests/browser/nextjs-16-edge/package.json new file mode 100644 index 00000000000..3668d76cde5 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/package.json @@ -0,0 +1,15 @@ +{ + "name": "nextjs-16-edge-bundle-test", + "type": "module", + "private": true, + "scripts": { + "build": "rm -rf .next && next build", + "test:bundle": "npm i && npx tsx next.test.ts" + }, + "dependencies": { + "@opentelemetry/resources": "file:../../../packages/opentelemetry-resources", + "next": "^16", + "react": "^19", + "react-dom": "^19" + } +} diff --git a/bundler-tests/browser/nextjs-16-edge/project.json b/bundler-tests/browser/nextjs-16-edge/project.json new file mode 100644 index 00000000000..4fe165dfc53 --- /dev/null +++ b/bundler-tests/browser/nextjs-16-edge/project.json @@ -0,0 +1,3 @@ +{ + "name": "nextjs-16-edge-bundle-test" +} diff --git a/packages/opentelemetry-resources/src/default-service-name.ts b/packages/opentelemetry-resources/src/default-service-name.ts index 887dddc73a2..88b2363796e 100644 --- a/packages/opentelemetry-resources/src/default-service-name.ts +++ b/packages/opentelemetry-resources/src/default-service-name.ts @@ -14,15 +14,26 @@ * limitations under the License. */ -// Check if we are in a Node.js environment and if so, use the process.argv0 property -// to determine the default service name -const DEFAULT_SERVICE_NAME = - typeof process === 'object' && - typeof process.argv0 === 'string' && - process.argv0.length > 0 - ? `unknown_service:${process.argv0}` - : 'unknown_service'; +let serviceName: string | undefined; +/** + * Returns the default service name for OpenTelemetry resources. + * In Node.js environments, returns "unknown_service:". + * In browser/edge environments, returns "unknown_service". + */ export function defaultServiceName(): string { - return DEFAULT_SERVICE_NAME; + if (serviceName === undefined) { + try { + const argv0 = globalThis.process.argv0; + serviceName = argv0 ? `unknown_service:${argv0}` : 'unknown_service'; + } catch { + serviceName = 'unknown_service'; + } + } + return serviceName; +} + +/** @internal For testing purposes only */ +export function _clearDefaultServiceNameCache(): void { + serviceName = undefined; } diff --git a/packages/opentelemetry-resources/test/Resource.test.ts b/packages/opentelemetry-resources/test/Resource.test.ts index e3056167941..1747deb795f 100644 --- a/packages/opentelemetry-resources/test/Resource.test.ts +++ b/packages/opentelemetry-resources/test/Resource.test.ts @@ -26,6 +26,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { describeBrowser, describeNode } from './util'; import { defaultResource, emptyResource, resourceFromAttributes } from '../src'; +import { _clearDefaultServiceNameCache } from '../src/default-service-name'; import * as EventEmitter from 'events'; describe('Resource', () => { @@ -43,6 +44,8 @@ describe('Resource', () => { 'k8s.io/location': 'location1', }); + beforeEach(() => _clearDefaultServiceNameCache()); + it('should return merged resource', () => { const expectedResource = resourceFromAttributes({ 'k8s.io/container/name': 'c1', diff --git a/packages/opentelemetry-resources/test/default-service-name.test.ts b/packages/opentelemetry-resources/test/default-service-name.test.ts index 7456d2a0162..a22e02fecc5 100644 --- a/packages/opentelemetry-resources/test/default-service-name.test.ts +++ b/packages/opentelemetry-resources/test/default-service-name.test.ts @@ -15,32 +15,49 @@ */ import * as assert from 'assert'; -import { defaultServiceName } from '../src/default-service-name'; - -const isNode = typeof process === 'object' && typeof process.argv0 === 'string'; +import { + _clearDefaultServiceNameCache, + defaultServiceName, +} from '../src/default-service-name'; +import { describeNode } from './util'; describe('defaultServiceName', () => { + const originalProcess = globalThis.process; + + beforeEach(() => _clearDefaultServiceNameCache()); + afterEach(() => { + globalThis.process = originalProcess; + }); + it('returns unknown_service prefix', () => { const serviceName = defaultServiceName(); assert.ok(serviceName.startsWith('unknown_service')); }); - if (isNode) { + it('returns consistent value on multiple calls', () => { + const serviceName1 = defaultServiceName(); + const serviceName2 = defaultServiceName(); + assert.strictEqual(serviceName1, serviceName2); + }); + + describeNode('defaultServiceName', () => { it('includes process.argv0 in Node.js', () => { const serviceName = defaultServiceName(); - assert.ok(serviceName.startsWith('unknown_service:')); - assert.ok(serviceName.length > 'unknown_service:'.length); + assert.match(serviceName, /^unknown_service:.+/); }); - } else { - it('returns plain unknown_service in browser', () => { + + it('returns plain unknown_service when process is not an object', () => { + // @ts-expect-error redefining process for testing + globalThis.process = undefined; const serviceName = defaultServiceName(); assert.strictEqual(serviceName, 'unknown_service'); }); - } - it('returns consistent value on multiple calls', () => { - const serviceName1 = defaultServiceName(); - const serviceName2 = defaultServiceName(); - assert.strictEqual(serviceName1, serviceName2); + it('returns plain unknown_service when process is stubbed but empty (edge runtimes)', () => { + // @ts-expect-error redefining process for testing + globalThis.process = {}; + const serviceName = defaultServiceName(); + assert.strictEqual(serviceName, 'unknown_service'); + }); }); });