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');
+ });
});
});